001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.io.hfile.bucket;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertNotEquals;
022import static org.junit.Assert.assertTrue;
023
024import java.io.BufferedWriter;
025import java.io.FileOutputStream;
026import java.io.OutputStreamWriter;
027import java.nio.file.FileSystems;
028import java.nio.file.Files;
029import java.nio.file.attribute.FileTime;
030import java.time.Instant;
031import java.util.Arrays;
032import org.apache.hadoop.fs.Path;
033import org.apache.hadoop.hbase.HBaseClassTestRule;
034import org.apache.hadoop.hbase.HBaseTestingUtility;
035import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
036import org.apache.hadoop.hbase.io.hfile.CacheTestUtils;
037import org.apache.hadoop.hbase.io.hfile.Cacheable;
038import org.apache.hadoop.hbase.testclassification.SmallTests;
039import org.junit.ClassRule;
040import org.junit.Test;
041import org.junit.experimental.categories.Category;
042import org.junit.runner.RunWith;
043import org.junit.runners.Parameterized;
044
045/**
046 * Basic test for check file's integrity before start BucketCache in fileIOEngine
047 */
048@RunWith(Parameterized.class)
049@Category(SmallTests.class)
050public class TestVerifyBucketCacheFile {
051  @ClassRule
052  public static final HBaseClassTestRule CLASS_RULE =
053    HBaseClassTestRule.forClass(TestVerifyBucketCacheFile.class);
054
055  @Parameterized.Parameters(name = "{index}: blockSize={0}, bucketSizes={1}")
056  public static Iterable<Object[]> data() {
057    return Arrays.asList(new Object[][] { { 8192, null },
058      { 16 * 1024,
059        new int[] { 2 * 1024 + 1024, 4 * 1024 + 1024, 8 * 1024 + 1024, 16 * 1024 + 1024,
060          28 * 1024 + 1024, 32 * 1024 + 1024, 64 * 1024 + 1024, 96 * 1024 + 1024,
061          128 * 1024 + 1024 } } });
062  }
063
064  @Parameterized.Parameter(0)
065  public int constructedBlockSize;
066
067  @Parameterized.Parameter(1)
068  public int[] constructedBlockSizes;
069
070  final long capacitySize = 32 * 1024 * 1024;
071  final int writeThreads = BucketCache.DEFAULT_WRITER_THREADS;
072  final int writerQLen = BucketCache.DEFAULT_WRITER_QUEUE_ITEMS;
073
074  /**
075   * Test cache file or persistence file does not exist whether BucketCache starts normally (1)
076   * Start BucketCache and add some blocks, then shutdown BucketCache and persist cache to file.
077   * Restart BucketCache and it can restore cache from file. (2) Delete bucket cache file after
078   * shutdown BucketCache. Restart BucketCache and it can't restore cache from file, the cache file
079   * and persistence file would be deleted before BucketCache start normally. (3) Delete persistence
080   * file after shutdown BucketCache. Restart BucketCache and it can't restore cache from file, the
081   * cache file and persistence file would be deleted before BucketCache start normally.
082   * @throws Exception the exception
083   */
084  @Test
085  public void testRetrieveFromFile() throws Exception {
086    HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
087    Path testDir = TEST_UTIL.getDataTestDir();
088    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
089
090    BucketCache bucketCache =
091      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
092        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
093    long usedSize = bucketCache.getAllocator().getUsedSize();
094    assertEquals(0, usedSize);
095    CacheTestUtils.HFileBlockPair[] blocks =
096      CacheTestUtils.generateHFileBlocks(constructedBlockSize, 1);
097    // Add blocks
098    for (CacheTestUtils.HFileBlockPair block : blocks) {
099      cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
100    }
101    usedSize = bucketCache.getAllocator().getUsedSize();
102    assertNotEquals(0, usedSize);
103    // 1.persist cache to file
104    bucketCache.shutdown();
105    // restore cache from file
106    bucketCache =
107      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
108        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
109    assertEquals(usedSize, bucketCache.getAllocator().getUsedSize());
110    // persist cache to file
111    bucketCache.shutdown();
112
113    // 2.delete bucket cache file
114    final java.nio.file.Path cacheFile =
115      FileSystems.getDefault().getPath(testDir.toString(), "bucket.cache");
116    assertTrue(Files.deleteIfExists(cacheFile));
117    // can't restore cache from file
118    bucketCache =
119      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
120        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
121    assertEquals(0, bucketCache.getAllocator().getUsedSize());
122    assertEquals(0, bucketCache.backingMap.size());
123    // Add blocks
124    for (CacheTestUtils.HFileBlockPair block : blocks) {
125      cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
126    }
127    usedSize = bucketCache.getAllocator().getUsedSize();
128    assertNotEquals(0, usedSize);
129    // persist cache to file
130    bucketCache.shutdown();
131
132    // 3.delete backingMap persistence file
133    final java.nio.file.Path mapFile =
134      FileSystems.getDefault().getPath(testDir.toString(), "bucket.persistence");
135    assertTrue(Files.deleteIfExists(mapFile));
136    // can't restore cache from file
137    bucketCache =
138      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
139        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
140    assertEquals(0, bucketCache.getAllocator().getUsedSize());
141    assertEquals(0, bucketCache.backingMap.size());
142
143    TEST_UTIL.cleanupTestDir();
144  }
145
146  /**
147   * Test whether BucketCache is started normally after modifying the cache file. Start BucketCache
148   * and add some blocks, then shutdown BucketCache and persist cache to file. Restart BucketCache
149   * after modify cache file's data, and it can't restore cache from file, the cache file and
150   * persistence file would be deleted before BucketCache start normally.
151   * @throws Exception the exception
152   */
153  @Test
154  public void testModifiedBucketCacheFileData() throws Exception {
155    HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
156    Path testDir = TEST_UTIL.getDataTestDir();
157    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
158
159    BucketCache bucketCache =
160      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
161        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
162    long usedSize = bucketCache.getAllocator().getUsedSize();
163    assertEquals(0, usedSize);
164
165    CacheTestUtils.HFileBlockPair[] blocks =
166      CacheTestUtils.generateHFileBlocks(constructedBlockSize, 1);
167    // Add blocks
168    for (CacheTestUtils.HFileBlockPair block : blocks) {
169      cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
170    }
171    usedSize = bucketCache.getAllocator().getUsedSize();
172    assertNotEquals(0, usedSize);
173    // persist cache to file
174    bucketCache.shutdown();
175
176    // modified bucket cache file
177    String file = testDir + "/bucket.cache";
178    try (BufferedWriter out =
179      new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false)))) {
180      out.write("test bucket cache");
181    }
182    // can't restore cache from file
183    bucketCache =
184      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
185        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
186    assertEquals(0, bucketCache.getAllocator().getUsedSize());
187    assertEquals(0, bucketCache.backingMap.size());
188
189    TEST_UTIL.cleanupTestDir();
190  }
191
192  /**
193   * Test whether BucketCache is started normally after modifying the cache file's last modified
194   * time. First Start BucketCache and add some blocks, then shutdown BucketCache and persist cache
195   * to file. Then Restart BucketCache after modify cache file's last modified time, and it can't
196   * restore cache from file, the cache file and persistence file would be deleted before
197   * BucketCache start normally.
198   * @throws Exception the exception
199   */
200  @Test
201  public void testModifiedBucketCacheFileTime() throws Exception {
202    HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
203    Path testDir = TEST_UTIL.getDataTestDir();
204    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
205
206    BucketCache bucketCache =
207      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
208        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
209    long usedSize = bucketCache.getAllocator().getUsedSize();
210    assertEquals(0, usedSize);
211
212    CacheTestUtils.HFileBlockPair[] blocks =
213      CacheTestUtils.generateHFileBlocks(constructedBlockSize, 1);
214    // Add blocks
215    for (CacheTestUtils.HFileBlockPair block : blocks) {
216      cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
217    }
218    usedSize = bucketCache.getAllocator().getUsedSize();
219    assertNotEquals(0, usedSize);
220    // persist cache to file
221    bucketCache.shutdown();
222
223    // modified bucket cache file LastModifiedTime
224    final java.nio.file.Path file =
225      FileSystems.getDefault().getPath(testDir.toString(), "bucket.cache");
226    Files.setLastModifiedTime(file, FileTime.from(Instant.now().plusMillis(1_000)));
227    // can't restore cache from file
228    bucketCache =
229      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
230        constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
231    assertEquals(0, bucketCache.getAllocator().getUsedSize());
232    assertEquals(0, bucketCache.backingMap.size());
233
234    TEST_UTIL.cleanupTestDir();
235  }
236
237  private void waitUntilFlushedToBucket(BucketCache cache, BlockCacheKey cacheKey)
238    throws InterruptedException {
239    while (!cache.backingMap.containsKey(cacheKey) || cache.ramCache.containsKey(cacheKey)) {
240      Thread.sleep(100);
241    }
242  }
243
244  // BucketCache.cacheBlock is async, it first adds block to ramCache and writeQueue, then writer
245  // threads will flush it to the bucket and put reference entry in backingMap.
246  private void cacheAndWaitUntilFlushedToBucket(BucketCache cache, BlockCacheKey cacheKey,
247    Cacheable block) throws InterruptedException {
248    cache.cacheBlock(cacheKey, block);
249    waitUntilFlushedToBucket(cache, cacheKey);
250  }
251}