/* * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 * (the "License"). You may not use this work except in compliance with the License, which is * available at www.apache.org/licenses/LICENSE-2.0 * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied, as more fully set forth in the License. * * See the NOTICE file distributed with this work for information regarding copyright ownership. */ package alluxio.worker.block.evictor; import alluxio.Configuration; import alluxio.PropertyKey; import alluxio.worker.block.BlockMetadataManager; import alluxio.worker.block.BlockMetadataManagerView; import alluxio.worker.block.BlockStoreEventListener; import alluxio.worker.block.BlockStoreLocation; import alluxio.worker.block.TieredBlockStoreTestUtils; import alluxio.worker.block.allocator.Allocator; import alluxio.worker.block.allocator.MaxFreeAllocator; import alluxio.worker.block.meta.StorageDir; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * Unit tests for specific behavior of {@link LRFUEvictor} such as evicting/moving blocks with * minimum CRF value and cascading LRFU eviction. */ public class LRFUEvictorTest { private static final long SESSION_ID = 2; private static final long BLOCK_ID = 10; private BlockMetadataManager mMetaManager; private BlockMetadataManagerView mManagerView; private Evictor mEvictor; private double mStepFactor; private double mAttenuationFactor; /** Rule to create a new temporary folder during each test. */ @Rule public TemporaryFolder mTestFolder = new TemporaryFolder(); /** * Sets up all dependencies before a test runs. */ @Before public final void before() throws Exception { File tempFolder = mTestFolder.newFolder(); mMetaManager = TieredBlockStoreTestUtils.defaultMetadataManager(tempFolder.getAbsolutePath()); mManagerView = new BlockMetadataManagerView(mMetaManager, Collections.<Long>emptySet(), Collections.<Long>emptySet()); Configuration.set(PropertyKey.WORKER_EVICTOR_CLASS, LRFUEvictor.class.getName()); Configuration.set(PropertyKey.WORKER_ALLOCATOR_CLASS, MaxFreeAllocator.class.getName()); Allocator allocator = Allocator.Factory.create(mManagerView); mStepFactor = Configuration.getDouble(PropertyKey.WORKER_EVICTOR_LRFU_STEP_FACTOR); mAttenuationFactor = Configuration.getDouble(PropertyKey.WORKER_EVICTOR_LRFU_ATTENUATION_FACTOR); mEvictor = Evictor.Factory.create(mManagerView, allocator); } /** * Caches bytes into {@link StorageDir}. * * @param sessionId session who caches the data * @param blockId id of the cached block * @param bytes size of the block in bytes * @param tierLevel tier level * @param dirIdx index of a directory */ private void cache(long sessionId, long blockId, long bytes, int tierLevel, int dirIdx) throws Exception { StorageDir dir = mMetaManager.getTiers().get(tierLevel).getDir(dirIdx); TieredBlockStoreTestUtils.cache(sessionId, blockId, bytes, dir, mMetaManager, mEvictor); } /** * Access the block to update {@link Evictor}. */ private void access(long blockId) { ((BlockStoreEventListener) mEvictor).onAccessBlock(SESSION_ID, blockId); } private double calculateAccessWeight(long timeInterval) { return Math.pow(1.0 / mAttenuationFactor, mStepFactor * timeInterval); } /** * Sort all blocks in ascending order of CRF. * * @return the sorted CRF of all blocks */ private List<Map.Entry<Long, Double>> getSortedCRF(Map<Long, Double> crfMap) { List<Map.Entry<Long, Double>> sortedCRF = new ArrayList<>(crfMap.entrySet()); Collections.sort(sortedCRF, new Comparator<Map.Entry<Long, Double>>() { @Override public int compare(Entry<Long, Double> o1, Entry<Long, Double> o2) { double res = o1.getValue() - o2.getValue(); if (res < 0) { return -1; } else if (res > 0) { return 1; } else { return 0; } } }); return sortedCRF; } /** * Tests that the eviction in the bottom tier works. */ @Test public void evictInBottomTier() throws Exception { int bottomTierOrdinal = TieredBlockStoreTestUtils .TIER_ORDINAL[TieredBlockStoreTestUtils.TIER_ORDINAL.length - 1]; Map<Long, Double> blockIdToCRF = new HashMap<>(); // capacity increases with index long[] bottomTierDirCapacity = TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[bottomTierOrdinal]; int nDir = bottomTierDirCapacity.length; // fill in dirs from larger to smaller capacity with blockId equal to BLOCK_ID plus dir index for (int i = 0; i < nDir; i++) { cache(SESSION_ID, BLOCK_ID + i, bottomTierDirCapacity[i], bottomTierOrdinal, i); // update CRF of blocks when blocks are committed blockIdToCRF.put(BLOCK_ID + i, calculateAccessWeight(nDir - 1 - i)); } // access blocks in the order: 10, 10, 11, 10, 11, 12. Update CRF of all blocks // during each access for (int i = 0; i < nDir; i++) { for (int j = 0; j <= i; j++) { access(BLOCK_ID + j); for (int k = 0; k < nDir; k++) { if (k == j) { blockIdToCRF.put(BLOCK_ID + k, blockIdToCRF.get(BLOCK_ID + k) * calculateAccessWeight(1L) + 1.0); } else { blockIdToCRF.put(BLOCK_ID + k, blockIdToCRF.get(BLOCK_ID + k) * calculateAccessWeight(1L)); } } } } // sort blocks in ascending order of CRF List<Entry<Long, Double>> blockCRF = getSortedCRF(blockIdToCRF); BlockStoreLocation anyDirInBottomTier = BlockStoreLocation.anyDirInTier(TieredBlockStoreTestUtils.TIER_ALIAS[bottomTierOrdinal]); // request smallest capacity and update access time on the evicted block for nDir times, the dir // to evict blocks from should be in the same order as sorted blockCRF for (int i = 0; i < nDir; i++) { EvictionPlan plan = mEvictor.freeSpaceWithView(bottomTierDirCapacity[0], anyDirInBottomTier, mManagerView); Assert.assertNotNull(plan); Assert.assertTrue(plan.toMove().isEmpty()); Assert.assertEquals(1, plan.toEvict().size()); long toEvictBlockId = plan.toEvict().get(0).getFirst(); long objectBlockId = blockCRF.get(i).getKey(); Assert.assertEquals(objectBlockId + " " + toEvictBlockId, objectBlockId, toEvictBlockId); // update CRF of the chosen block in case that it is chosen again for (int j = 0; j < nDir; j++) { access(toEvictBlockId); } } } /** * Tests the cascading eviction with the first tier filled and the second tier empty resulting in * no eviction. */ @Test public void cascadingEvictionTest1() throws Exception { // Two tiers, each dir in the second tier has more space than any dir in the first tier. Fill in // the first tier, leave the second tier empty. Request space from the first tier, blocks should // be moved from the first to the second tier without eviction. int firstTierOrdinal = TieredBlockStoreTestUtils.TIER_ORDINAL[0]; long[] firstTierDirCapacity = TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[0]; int nDir = firstTierDirCapacity.length; Map<Long, Double> blockIdToCRF = new HashMap<>(); for (int i = 0; i < nDir; i++) { cache(SESSION_ID, BLOCK_ID + i, firstTierDirCapacity[i], firstTierOrdinal, i); // update CRF of blocks when blocks are committed blockIdToCRF.put(BLOCK_ID + i, calculateAccessWeight(nDir - 1 - i)); } // access blocks in the order: 10, 10, 11. Update CRF of all blocks // during each access for (int i = 0; i < nDir; i++) { for (int j = 0; j <= i; j++) { access(BLOCK_ID + j); for (int k = 0; k < nDir; k++) { if (k == j) { blockIdToCRF.put(BLOCK_ID + k, blockIdToCRF.get(BLOCK_ID + k) * calculateAccessWeight(1L) + 1.0); } else { blockIdToCRF.put(BLOCK_ID + k, blockIdToCRF.get(BLOCK_ID + k) * calculateAccessWeight(1L)); } } } } List<Map.Entry<Long, Double>> blockCRF = getSortedCRF(blockIdToCRF); BlockStoreLocation anyDirInFirstTier = BlockStoreLocation.anyDirInTier(TieredBlockStoreTestUtils.TIER_ALIAS[firstTierOrdinal]); long smallestCapacity = firstTierDirCapacity[0]; // request smallest capacity and update access time on the moved block for nDir times, the dir // to move blocks from should be in the same order as sorted blockCRF for (int i = 0; i < nDir; i++) { EvictionPlan plan = mEvictor.freeSpaceWithView(smallestCapacity, anyDirInFirstTier, mManagerView); Assert.assertTrue(EvictorTestUtils.validCascadingPlan(smallestCapacity, plan, mMetaManager)); Assert.assertEquals(0, plan.toEvict().size()); Assert.assertEquals(1, plan.toMove().size()); long blockId = plan.toMove().get(0).getBlockId(); long objectBlockId = blockCRF.get(i).getKey(); Assert.assertEquals(objectBlockId, blockId); // update CRF of the chosen block in case that it is chosen again for (int j = 0; j < nDir; j++) { access(objectBlockId); } } } /** * Tests the cascading eviction with the first and second tier filled resulting in blocks in the * second tier are evicted. */ @Test public void cascadingEvictionTest2() throws Exception { // Two tiers, the second tier has more dirs than the first tier and each dir in the second tier // has more space than any dir in the first tier. Fill in all dirs and request space from the // first tier, blocks should be moved from the first to the second tier, and some blocks in the // second tier should be evicted to hold blocks moved from the first tier. long blockId = BLOCK_ID; long totalBlocks = 0; for (int tierOrdinal : TieredBlockStoreTestUtils.TIER_ORDINAL) { totalBlocks += TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[tierOrdinal].length; } Map<Long, Double> blockIdToCRF = new HashMap<>(); for (int tierOrdinal : TieredBlockStoreTestUtils.TIER_ORDINAL) { long[] tierCapacity = TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[tierOrdinal]; for (int dirIdx = 0; dirIdx < tierCapacity.length; dirIdx++) { cache(SESSION_ID, blockId, tierCapacity[dirIdx], tierOrdinal, dirIdx); // update CRF of blocks when blocks are committed blockIdToCRF.put(blockId, calculateAccessWeight(totalBlocks - 1 - (blockId - BLOCK_ID))); blockId++; } } // access blocks in the order: 10, 10, 11, 10, 11, 12, 10, 11, 12, 13, 10, 11, 12, 13, 14 // Update CRF of all blocks during each access for (int i = 0; i < totalBlocks; i++) { for (int j = 0; j <= i; j++) { access(BLOCK_ID + j); for (int k = 0; k < totalBlocks; k++) { if (k == j) { blockIdToCRF.put(BLOCK_ID + k, blockIdToCRF.get(BLOCK_ID + k) * calculateAccessWeight(1L) + 1.0); } else { blockIdToCRF.put(BLOCK_ID + k, blockIdToCRF.get(BLOCK_ID + k) * calculateAccessWeight(1L)); } } } } List<Map.Entry<Long, Double>> blockCRF = getSortedCRF(blockIdToCRF); // sorted blocks in the first tier List<Long> blocksInFirstTier = new ArrayList<>(); // sorted blocks in the second tier List<Long> blocksInSecondTier = new ArrayList<>(); for (int i = 0; i < blockCRF.size(); i++) { long block = blockCRF.get(i).getKey(); if (block - BLOCK_ID < TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[0].length) { blocksInFirstTier.add(block); } else if (block - BLOCK_ID < TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[0].length + TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[1].length) { blocksInSecondTier.add(block); } } BlockStoreLocation anyDirInFirstTier = BlockStoreLocation.anyDirInTier(TieredBlockStoreTestUtils.TIER_ALIAS[0]); int nDirInFirstTier = TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[0].length; long smallestCapacity = TieredBlockStoreTestUtils.TIER_CAPACITY_BYTES[0][0]; for (int i = 0; i < nDirInFirstTier; i++) { EvictionPlan plan = mEvictor.freeSpaceWithView(smallestCapacity, anyDirInFirstTier, mManagerView); Assert.assertTrue(EvictorTestUtils.validCascadingPlan(smallestCapacity, plan, mMetaManager)); // block with minimum CRF in the first tier needs to be moved to the second tier Assert.assertEquals(1, plan.toMove().size()); long blockIdMovedInFirstTier = plan.toMove().get(0).getBlockId(); long objectBlockIdInFirstTier = blocksInFirstTier.get(i); Assert.assertEquals(objectBlockIdInFirstTier, blockIdMovedInFirstTier); // cached block with minimum CRF in the second tier will be evicted to hold blocks moved // from first tier Assert.assertEquals(1, plan.toEvict().size()); long blockIdEvictedInSecondTier = plan.toEvict().get(0).getFirst(); long objectBlockIdInSecondTier = blocksInSecondTier.get(i); Assert.assertEquals(objectBlockIdInSecondTier, blockIdEvictedInSecondTier); // update CRF of the chosen blocks in case that they are chosen again for (int j = 0; j < totalBlocks; j++) { access(blockIdMovedInFirstTier); } for (int j = 0; j < totalBlocks; j++) { access(blockIdEvictedInSecondTier); } } } }