|  | @@ -34,8 +34,10 @@ import org.elasticsearch.threadpool.ThreadPool;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import java.io.IOException;
 | 
	
		
			
				|  |  |  import java.util.Collection;
 | 
	
		
			
				|  |  | +import java.util.HashMap;
 | 
	
		
			
				|  |  |  import java.util.HashSet;
 | 
	
		
			
				|  |  |  import java.util.List;
 | 
	
		
			
				|  |  | +import java.util.Map;
 | 
	
		
			
				|  |  |  import java.util.Objects;
 | 
	
		
			
				|  |  |  import java.util.Set;
 | 
	
		
			
				|  |  |  import java.util.concurrent.BrokenBarrierException;
 | 
	
	
		
			
				|  | @@ -90,11 +92,11 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertEquals(2, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              synchronized (cacheService) {
 | 
	
		
			
				|  |  | -                assertTrue(region1.tryEvict());
 | 
	
		
			
				|  |  | +                assertTrue(tryEvict(region1));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              assertEquals(3, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |              synchronized (cacheService) {
 | 
	
		
			
				|  |  | -                assertFalse(region1.tryEvict());
 | 
	
		
			
				|  |  | +                assertFalse(tryEvict(region1));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              assertEquals(3, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |              final var bytesReadFuture = new PlainActionFuture<Integer>();
 | 
	
	
		
			
				|  | @@ -107,17 +109,17 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  bytesReadFuture
 | 
	
		
			
				|  |  |              );
 | 
	
		
			
				|  |  |              synchronized (cacheService) {
 | 
	
		
			
				|  |  | -                assertFalse(region0.tryEvict());
 | 
	
		
			
				|  |  | +                assertFalse(tryEvict(region0));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              assertEquals(3, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |              assertFalse(bytesReadFuture.isDone());
 | 
	
		
			
				|  |  |              taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  |              synchronized (cacheService) {
 | 
	
		
			
				|  |  | -                assertTrue(region0.tryEvict());
 | 
	
		
			
				|  |  | +                assertTrue(tryEvict(region0));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              assertEquals(4, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |              synchronized (cacheService) {
 | 
	
		
			
				|  |  | -                assertTrue(region2.tryEvict());
 | 
	
		
			
				|  |  | +                assertTrue(tryEvict(region2));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              assertEquals(5, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |              assertTrue(bytesReadFuture.isDone());
 | 
	
	
		
			
				|  | @@ -125,6 +127,18 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private static boolean tryEvict(SharedBlobCacheService<Object>.CacheFileRegion region1) {
 | 
	
		
			
				|  |  | +        if (randomBoolean()) {
 | 
	
		
			
				|  |  | +            return region1.tryEvict();
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            boolean result = region1.tryEvictNoDecRef();
 | 
	
		
			
				|  |  | +            if (result) {
 | 
	
		
			
				|  |  | +                region1.decRef();
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            return result;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      public void testAutoEviction() throws IOException {
 | 
	
		
			
				|  |  |          Settings settings = Settings.builder()
 | 
	
		
			
				|  |  |              .put(NODE_NAME_SETTING.getKey(), "node")
 | 
	
	
		
			
				|  | @@ -163,7 +177,7 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              // explicitly evict region 1
 | 
	
		
			
				|  |  |              synchronized (cacheService) {
 | 
	
		
			
				|  |  | -                assertTrue(region1.tryEvict());
 | 
	
		
			
				|  |  | +                assertTrue(tryEvict(region1));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              assertEquals(1, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |          }
 | 
	
	
		
			
				|  | @@ -237,9 +251,10 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public void testDecay() throws IOException {
 | 
	
		
			
				|  |  | +        // we have 8 regions
 | 
	
		
			
				|  |  |          Settings settings = Settings.builder()
 | 
	
		
			
				|  |  |              .put(NODE_NAME_SETTING.getKey(), "node")
 | 
	
		
			
				|  |  | -            .put(SharedBlobCacheService.SHARED_CACHE_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(size(500)).getStringRep())
 | 
	
		
			
				|  |  | +            .put(SharedBlobCacheService.SHARED_CACHE_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(size(400)).getStringRep())
 | 
	
		
			
				|  |  |              .put(SharedBlobCacheService.SHARED_CACHE_REGION_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(size(100)).getStringRep())
 | 
	
		
			
				|  |  |              .put("path.home", createTempDir())
 | 
	
		
			
				|  |  |              .build();
 | 
	
	
		
			
				|  | @@ -254,51 +269,152 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  BlobCacheMetrics.NOOP
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          ) {
 | 
	
		
			
				|  |  | +            assertEquals(4, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              final var cacheKey1 = generateCacheKey();
 | 
	
		
			
				|  |  |              final var cacheKey2 = generateCacheKey();
 | 
	
		
			
				|  |  | -            assertEquals(5, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  | +            final var cacheKey3 = generateCacheKey();
 | 
	
		
			
				|  |  | +            // add a region that we can evict when provoking first decay
 | 
	
		
			
				|  |  | +            cacheService.get("evictkey", size(250), 0);
 | 
	
		
			
				|  |  | +            assertEquals(3, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |              final var region0 = cacheService.get(cacheKey1, size(250), 0);
 | 
	
		
			
				|  |  | -            assertEquals(4, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  | +            assertEquals(2, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |              final var region1 = cacheService.get(cacheKey2, size(250), 1);
 | 
	
		
			
				|  |  | -            assertEquals(3, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  | +            assertEquals(1, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  | +            final var region2 = cacheService.get(cacheKey3, size(250), 1);
 | 
	
		
			
				|  |  | +            assertEquals(0, cacheService.freeRegionCount());
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              assertEquals(1, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  |              assertEquals(1, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(1, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  | +            AtomicLong expectedEpoch = new AtomicLong();
 | 
	
		
			
				|  |  | +            Runnable triggerDecay = () -> {
 | 
	
		
			
				|  |  | +                assertThat(taskQueue.hasRunnableTasks(), is(false));
 | 
	
		
			
				|  |  | +                cacheService.get(expectedEpoch.toString(), size(250), 0);
 | 
	
		
			
				|  |  | +                assertThat(taskQueue.hasRunnableTasks(), is(true));
 | 
	
		
			
				|  |  | +                taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | +                assertThat(cacheService.epoch(), equalTo(expectedEpoch.incrementAndGet()));
 | 
	
		
			
				|  |  | +            };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            triggerDecay.run();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            taskQueue.advanceTime();
 | 
	
		
			
				|  |  | -            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey1, size(250), 0);
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey2, size(250), 1);
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey3, size(250), 1);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            triggerDecay.run();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              final var region0Again = cacheService.get(cacheKey1, size(250), 0);
 | 
	
		
			
				|  |  |              assertSame(region0Again, region0);
 | 
	
		
			
				|  |  | -            assertEquals(2, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  | +            assertEquals(3, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  |              assertEquals(1, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(1, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            triggerDecay.run();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            taskQueue.advanceTime();
 | 
	
		
			
				|  |  | -            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  |              cacheService.get(cacheKey1, size(250), 0);
 | 
	
		
			
				|  |  | -            assertEquals(3, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  | +            assertEquals(4, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  |              cacheService.get(cacheKey1, size(250), 0);
 | 
	
		
			
				|  |  | +            assertEquals(4, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  | +            assertEquals(0, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(0, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            // ensure no freq=0 entries
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey2, size(250), 1);
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey3, size(250), 1);
 | 
	
		
			
				|  |  | +            assertEquals(2, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(2, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            triggerDecay.run();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              assertEquals(3, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  | +            assertEquals(1, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(1, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            // advance 2 ticks (decay only starts after 2 ticks)
 | 
	
		
			
				|  |  | -            taskQueue.advanceTime();
 | 
	
		
			
				|  |  | -            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | -            taskQueue.advanceTime();
 | 
	
		
			
				|  |  | -            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | +            triggerDecay.run();
 | 
	
		
			
				|  |  |              assertEquals(2, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  |              assertEquals(0, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(0, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            // advance another tick
 | 
	
		
			
				|  |  | -            taskQueue.advanceTime();
 | 
	
		
			
				|  |  | -            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | +            // ensure no freq=0 entries
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey2, size(250), 1);
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey3, size(250), 1);
 | 
	
		
			
				|  |  | +            assertEquals(2, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(2, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            triggerDecay.run();
 | 
	
		
			
				|  |  |              assertEquals(1, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  | -            assertEquals(0, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(1, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(1, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            // advance another tick
 | 
	
		
			
				|  |  | -            taskQueue.advanceTime();
 | 
	
		
			
				|  |  | -            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | +            triggerDecay.run();
 | 
	
		
			
				|  |  |              assertEquals(0, cacheService.getFreq(region0));
 | 
	
		
			
				|  |  |              assertEquals(0, cacheService.getFreq(region1));
 | 
	
		
			
				|  |  | +            assertEquals(0, cacheService.getFreq(region2));
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * Test when many objects need to decay, in particular useful to measure how long the decay task takes.
 | 
	
		
			
				|  |  | +     * For 1M objects (with no assertions) it took 26ms locally.
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    public void testMassiveDecay() throws IOException {
 | 
	
		
			
				|  |  | +        int regions = 1024; // to measure decay time, increase to 1024*1024 and disable assertions.
 | 
	
		
			
				|  |  | +        Settings settings = Settings.builder()
 | 
	
		
			
				|  |  | +            .put(NODE_NAME_SETTING.getKey(), "node")
 | 
	
		
			
				|  |  | +            .put(SharedBlobCacheService.SHARED_CACHE_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(size(regions)).getStringRep())
 | 
	
		
			
				|  |  | +            .put(SharedBlobCacheService.SHARED_CACHE_REGION_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(size(1)).getStringRep())
 | 
	
		
			
				|  |  | +            .put("path.home", createTempDir())
 | 
	
		
			
				|  |  | +            .build();
 | 
	
		
			
				|  |  | +        final DeterministicTaskQueue taskQueue = new DeterministicTaskQueue();
 | 
	
		
			
				|  |  | +        try (
 | 
	
		
			
				|  |  | +            NodeEnvironment environment = new NodeEnvironment(settings, TestEnvironment.newEnvironment(settings));
 | 
	
		
			
				|  |  | +            var cacheService = new SharedBlobCacheService<>(
 | 
	
		
			
				|  |  | +                environment,
 | 
	
		
			
				|  |  | +                settings,
 | 
	
		
			
				|  |  | +                taskQueue.getThreadPool(),
 | 
	
		
			
				|  |  | +                ThreadPool.Names.GENERIC,
 | 
	
		
			
				|  |  | +                BlobCacheMetrics.NOOP
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  | +        ) {
 | 
	
		
			
				|  |  | +            Runnable decay = () -> {
 | 
	
		
			
				|  |  | +                assertThat(taskQueue.hasRunnableTasks(), is(true));
 | 
	
		
			
				|  |  | +                long before = System.currentTimeMillis();
 | 
	
		
			
				|  |  | +                taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | +                long after = System.currentTimeMillis();
 | 
	
		
			
				|  |  | +                logger.debug("took {} ms", (after - before));
 | 
	
		
			
				|  |  | +            };
 | 
	
		
			
				|  |  | +            long fileLength = size(regions + 100);
 | 
	
		
			
				|  |  | +            Object cacheKey = new Object();
 | 
	
		
			
				|  |  | +            for (int i = 0; i < regions; ++i) {
 | 
	
		
			
				|  |  | +                cacheService.get(cacheKey, fileLength, i);
 | 
	
		
			
				|  |  | +                if (Integer.bitCount(i) == 1) {
 | 
	
		
			
				|  |  | +                    logger.debug("did {} gets", i);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            assertThat(taskQueue.hasRunnableTasks(), is(false));
 | 
	
		
			
				|  |  | +            cacheService.get(cacheKey, fileLength, regions);
 | 
	
		
			
				|  |  | +            decay.run();
 | 
	
		
			
				|  |  | +            int maxRounds = 5;
 | 
	
		
			
				|  |  | +            for (int round = 2; round <= maxRounds; ++round) {
 | 
	
		
			
				|  |  | +                for (int i = round; i < regions + round; ++i) {
 | 
	
		
			
				|  |  | +                    cacheService.get(cacheKey, fileLength, i);
 | 
	
		
			
				|  |  | +                    if (Integer.bitCount(i) == 1) {
 | 
	
		
			
				|  |  | +                        logger.debug("did {} gets", i);
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                decay.run();
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            Map<Integer, Integer> freqs = new HashMap<>();
 | 
	
		
			
				|  |  | +            for (int i = maxRounds; i < regions + maxRounds; ++i) {
 | 
	
		
			
				|  |  | +                int freq = cacheService.getFreq(cacheService.get(cacheKey, fileLength, i)) - 2;
 | 
	
		
			
				|  |  | +                freqs.compute(freq, (k, v) -> v == null ? 1 : v + 1);
 | 
	
		
			
				|  |  | +                if (Integer.bitCount(i) == 1) {
 | 
	
		
			
				|  |  | +                    logger.debug("did {} gets", i);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            assertThat(freqs.get(4), equalTo(regions - maxRounds + 1));
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -308,12 +424,12 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  |      public void testGetMultiThreaded() throws IOException {
 | 
	
		
			
				|  |  |          int threads = between(2, 10);
 | 
	
		
			
				|  |  | +        int regionCount = between(1, 20);
 | 
	
		
			
				|  |  | +        // if we have enough regions, a get should always have a result (except for explicit evict interference)
 | 
	
		
			
				|  |  | +        final boolean allowAlreadyClosed = regionCount < threads;
 | 
	
		
			
				|  |  |          Settings settings = Settings.builder()
 | 
	
		
			
				|  |  |              .put(NODE_NAME_SETTING.getKey(), "node")
 | 
	
		
			
				|  |  | -            .put(
 | 
	
		
			
				|  |  | -                SharedBlobCacheService.SHARED_CACHE_SIZE_SETTING.getKey(),
 | 
	
		
			
				|  |  | -                ByteSizeValue.ofBytes(size(between(1, 20) * 100L)).getStringRep()
 | 
	
		
			
				|  |  | -            )
 | 
	
		
			
				|  |  | +            .put(SharedBlobCacheService.SHARED_CACHE_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(size(regionCount * 100L)).getStringRep())
 | 
	
		
			
				|  |  |              .put(SharedBlobCacheService.SHARED_CACHE_REGION_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(size(100)).getStringRep())
 | 
	
		
			
				|  |  |              .put(SharedBlobCacheService.SHARED_CACHE_MIN_TIME_DELTA_SETTING.getKey(), randomFrom("0", "1ms", "10s"))
 | 
	
		
			
				|  |  |              .put("path.home", createTempDir())
 | 
	
	
		
			
				|  | @@ -343,11 +459,13 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                          ready.await();
 | 
	
		
			
				|  |  |                          for (int i = 0; i < iterations; ++i) {
 | 
	
		
			
				|  |  |                              try {
 | 
	
		
			
				|  |  | -                                SharedBlobCacheService<String>.CacheFileRegion cacheFileRegion = cacheService.get(
 | 
	
		
			
				|  |  | -                                    cacheKeys[i],
 | 
	
		
			
				|  |  | -                                    fileLength,
 | 
	
		
			
				|  |  | -                                    regions[i]
 | 
	
		
			
				|  |  | -                                );
 | 
	
		
			
				|  |  | +                                SharedBlobCacheService<String>.CacheFileRegion cacheFileRegion;
 | 
	
		
			
				|  |  | +                                try {
 | 
	
		
			
				|  |  | +                                    cacheFileRegion = cacheService.get(cacheKeys[i], fileLength, regions[i]);
 | 
	
		
			
				|  |  | +                                } catch (AlreadyClosedException e) {
 | 
	
		
			
				|  |  | +                                    assert allowAlreadyClosed || e.getMessage().equals("evicted during free region allocation") : e;
 | 
	
		
			
				|  |  | +                                    throw e;
 | 
	
		
			
				|  |  | +                                }
 | 
	
		
			
				|  |  |                                  if (cacheFileRegion.tryIncRef()) {
 | 
	
		
			
				|  |  |                                      if (yield[i] == 0) {
 | 
	
		
			
				|  |  |                                          Thread.yield();
 | 
	
	
		
			
				|  | @@ -415,8 +533,7 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  threadPool,
 | 
	
		
			
				|  |  |                  ThreadPool.Names.GENERIC,
 | 
	
		
			
				|  |  |                  "bulk",
 | 
	
		
			
				|  |  | -                BlobCacheMetrics.NOOP,
 | 
	
		
			
				|  |  | -                threadPool::relativeTimeInMillis
 | 
	
		
			
				|  |  | +                BlobCacheMetrics.NOOP
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          ) {
 | 
	
		
			
				|  |  |              {
 | 
	
	
		
			
				|  | @@ -477,8 +594,7 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  threadPool,
 | 
	
		
			
				|  |  |                  ThreadPool.Names.GENERIC,
 | 
	
		
			
				|  |  |                  "bulk",
 | 
	
		
			
				|  |  | -                BlobCacheMetrics.NOOP,
 | 
	
		
			
				|  |  | -                threadPool::relativeTimeInMillis
 | 
	
		
			
				|  |  | +                BlobCacheMetrics.NOOP
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          ) {
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -708,8 +824,7 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public void testMaybeEvictLeastUsed() throws Exception {
 | 
	
		
			
				|  |  | -        final int numRegions = 3;
 | 
	
		
			
				|  |  | -        randomIntBetween(1, 500);
 | 
	
		
			
				|  |  | +        final int numRegions = 10;
 | 
	
		
			
				|  |  |          final long regionSize = size(1L);
 | 
	
		
			
				|  |  |          Settings settings = Settings.builder()
 | 
	
		
			
				|  |  |              .put(NODE_NAME_SETTING.getKey(), "node")
 | 
	
	
		
			
				|  | @@ -728,11 +843,10 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  taskQueue.getThreadPool(),
 | 
	
		
			
				|  |  |                  ThreadPool.Names.GENERIC,
 | 
	
		
			
				|  |  |                  "bulk",
 | 
	
		
			
				|  |  | -                BlobCacheMetrics.NOOP,
 | 
	
		
			
				|  |  | -                relativeTimeInMillis::get
 | 
	
		
			
				|  |  | +                BlobCacheMetrics.NOOP
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          ) {
 | 
	
		
			
				|  |  | -            final Set<Object> cacheKeys = new HashSet<>();
 | 
	
		
			
				|  |  | +            final Map<Object, SharedBlobCacheService<Object>.CacheFileRegion> cacheEntries = new HashMap<>();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              assertThat("All regions are free", cacheService.freeRegionCount(), equalTo(numRegions));
 | 
	
		
			
				|  |  |              assertThat("Cache has no entries", cacheService.maybeEvictLeastUsed(), is(false));
 | 
	
	
		
			
				|  | @@ -748,8 +862,7 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                      ActionListener.noop()
 | 
	
		
			
				|  |  |                  );
 | 
	
		
			
				|  |  |                  assertThat(cacheService.getFreq(entry), equalTo(1));
 | 
	
		
			
				|  |  | -                relativeTimeInMillis.incrementAndGet();
 | 
	
		
			
				|  |  | -                cacheKeys.add(cacheKey);
 | 
	
		
			
				|  |  | +                cacheEntries.put(cacheKey, entry);
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              assertThat("All regions are used", cacheService.freeRegionCount(), equalTo(0));
 | 
	
	
		
			
				|  | @@ -760,33 +873,41 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |              assertThat("All regions are used", cacheService.freeRegionCount(), equalTo(0));
 | 
	
		
			
				|  |  |              assertThat("Cache entries are not old enough to be evicted", cacheService.maybeEvictLeastUsed(), is(false));
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            // simulate elapsed time
 | 
	
		
			
				|  |  | -            var minInternalMillis = SharedBlobCacheService.SHARED_CACHE_MIN_TIME_DELTA_SETTING.getDefault(Settings.EMPTY).millis();
 | 
	
		
			
				|  |  | -            relativeTimeInMillis.addAndGet(minInternalMillis);
 | 
	
		
			
				|  |  | +            cacheService.maybeScheduleDecayAndNewEpoch();
 | 
	
		
			
				|  |  | +            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            cacheEntries.keySet().forEach(key -> cacheService.get(key, regionSize, 0));
 | 
	
		
			
				|  |  | +            cacheService.maybeScheduleDecayAndNewEpoch();
 | 
	
		
			
				|  |  | +            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              // touch some random cache entries
 | 
	
		
			
				|  |  | -            var unusedCacheKeys = Set.copyOf(randomSubsetOf(cacheKeys));
 | 
	
		
			
				|  |  | -            cacheKeys.forEach(key -> {
 | 
	
		
			
				|  |  | -                if (unusedCacheKeys.contains(key) == false) {
 | 
	
		
			
				|  |  | -                    var entry = cacheService.get(key, regionSize, 0);
 | 
	
		
			
				|  |  | -                    assertThat(cacheService.getFreq(entry), equalTo(2));
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | -            });
 | 
	
		
			
				|  |  | +            var usedCacheKeys = Set.copyOf(randomSubsetOf(cacheEntries.keySet()));
 | 
	
		
			
				|  |  | +            usedCacheKeys.forEach(key -> cacheService.get(key, regionSize, 0));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            cacheEntries.forEach(
 | 
	
		
			
				|  |  | +                (key, entry) -> assertThat(cacheService.getFreq(entry), usedCacheKeys.contains(key) ? equalTo(3) : equalTo(1))
 | 
	
		
			
				|  |  | +            );
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              assertThat("All regions are used", cacheService.freeRegionCount(), equalTo(0));
 | 
	
		
			
				|  |  |              assertThat("Cache entries are not old enough to be evicted", cacheService.maybeEvictLeastUsed(), is(false));
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            for (int i = 1; i <= unusedCacheKeys.size(); i++) {
 | 
	
		
			
				|  |  | -                // need to advance time and compute decay to decrease frequencies in cache and have an evictable entry
 | 
	
		
			
				|  |  | -                relativeTimeInMillis.addAndGet(minInternalMillis);
 | 
	
		
			
				|  |  | -                cacheService.computeDecay();
 | 
	
		
			
				|  |  | +            cacheService.maybeScheduleDecayAndNewEpoch();
 | 
	
		
			
				|  |  | +            taskQueue.runAllRunnableTasks();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                assertThat("Cache entry is old enough to be evicted", cacheService.maybeEvictLeastUsed(), is(true));
 | 
	
		
			
				|  |  | +            assertThat("All regions are used", cacheService.freeRegionCount(), equalTo(0));
 | 
	
		
			
				|  |  | +            cacheEntries.forEach(
 | 
	
		
			
				|  |  | +                (key, entry) -> assertThat(cacheService.getFreq(entry), usedCacheKeys.contains(key) ? equalTo(2) : equalTo(0))
 | 
	
		
			
				|  |  | +            );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            var zeroFrequencyCacheEntries = cacheEntries.size() - usedCacheKeys.size();
 | 
	
		
			
				|  |  | +            for (int i = 0; i < zeroFrequencyCacheEntries; i++) {
 | 
	
		
			
				|  |  |                  assertThat(cacheService.freeRegionCount(), equalTo(i));
 | 
	
		
			
				|  |  | +                assertThat("Cache entry is old enough to be evicted", cacheService.maybeEvictLeastUsed(), is(true));
 | 
	
		
			
				|  |  | +                assertThat(cacheService.freeRegionCount(), equalTo(i + 1));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              assertThat("No more cache entries old enough to be evicted", cacheService.maybeEvictLeastUsed(), is(false));
 | 
	
		
			
				|  |  | -            assertThat(cacheService.freeRegionCount(), equalTo(unusedCacheKeys.size()));
 | 
	
		
			
				|  |  | +            assertThat(cacheService.freeRegionCount(), equalTo(zeroFrequencyCacheEntries));
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -817,7 +938,6 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  return generic;
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  | -        final AtomicLong relativeTimeInMillis = new AtomicLong(0L);
 | 
	
		
			
				|  |  |          try (
 | 
	
		
			
				|  |  |              NodeEnvironment environment = new NodeEnvironment(settings, TestEnvironment.newEnvironment(settings));
 | 
	
		
			
				|  |  |              var cacheService = new SharedBlobCacheService<>(
 | 
	
	
		
			
				|  | @@ -826,8 +946,7 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  threadPool,
 | 
	
		
			
				|  |  |                  ThreadPool.Names.GENERIC,
 | 
	
		
			
				|  |  |                  "bulk",
 | 
	
		
			
				|  |  | -                BlobCacheMetrics.NOOP,
 | 
	
		
			
				|  |  | -                relativeTimeInMillis::get
 | 
	
		
			
				|  |  | +                BlobCacheMetrics.NOOP
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          ) {
 | 
	
		
			
				|  |  |              {
 | 
	
	
		
			
				|  | @@ -860,7 +979,6 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  final PlainActionFuture<Collection<Boolean>> future = new PlainActionFuture<>();
 | 
	
		
			
				|  |  |                  final var listener = new GroupedActionListener<>(remainingFreeRegions, future);
 | 
	
		
			
				|  |  |                  for (int region = 0; region < remainingFreeRegions; region++) {
 | 
	
		
			
				|  |  | -                    relativeTimeInMillis.addAndGet(1_000L);
 | 
	
		
			
				|  |  |                      cacheService.maybeFetchRegion(
 | 
	
		
			
				|  |  |                          cacheKey,
 | 
	
		
			
				|  |  |                          region,
 | 
	
	
		
			
				|  | @@ -897,9 +1015,6 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  assertThat("Region already exists in cache", future.get(), is(false));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | -                // simulate elapsed time and compute decay
 | 
	
		
			
				|  |  | -                var minInternalMillis = SharedBlobCacheService.SHARED_CACHE_MIN_TIME_DELTA_SETTING.getDefault(Settings.EMPTY).millis();
 | 
	
		
			
				|  |  | -                relativeTimeInMillis.addAndGet(minInternalMillis * 2);
 | 
	
		
			
				|  |  |                  cacheService.computeDecay();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  // fetch one more region should evict an old cache entry
 | 
	
	
		
			
				|  | @@ -942,8 +1057,7 @@ public class SharedBlobCacheServiceTests extends ESTestCase {
 | 
	
		
			
				|  |  |                  taskQueue.getThreadPool(),
 | 
	
		
			
				|  |  |                  ThreadPool.Names.GENERIC,
 | 
	
		
			
				|  |  |                  ThreadPool.Names.GENERIC,
 | 
	
		
			
				|  |  | -                BlobCacheMetrics.NOOP,
 | 
	
		
			
				|  |  | -                relativeTimeInMillis::get
 | 
	
		
			
				|  |  | +                BlobCacheMetrics.NOOP
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          ) {
 | 
	
		
			
				|  |  |              final var cacheKey = generateCacheKey();
 |