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.mob;
019
020import static org.apache.hadoop.hbase.mob.MobConstants.MOB_CLEANER_BATCH_SIZE_UPPER_BOUND;
021import static org.apache.hadoop.hbase.mob.MobConstants.MOB_CLEANER_THREAD_COUNT;
022import static org.junit.Assert.assertEquals;
023
024import java.io.IOException;
025import org.apache.hadoop.fs.FileStatus;
026import org.apache.hadoop.fs.Path;
027import org.apache.hadoop.hbase.HBaseClassTestRule;
028import org.apache.hadoop.hbase.HBaseTestingUtil;
029import org.apache.hadoop.hbase.TableName;
030import org.apache.hadoop.hbase.client.Admin;
031import org.apache.hadoop.hbase.client.BufferedMutator;
032import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
033import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
034import org.apache.hadoop.hbase.client.ConnectionFactory;
035import org.apache.hadoop.hbase.client.Put;
036import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
037import org.apache.hadoop.hbase.testclassification.MasterTests;
038import org.apache.hadoop.hbase.testclassification.MediumTests;
039import org.apache.hadoop.hbase.util.Bytes;
040import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
041import org.junit.After;
042import org.junit.AfterClass;
043import org.junit.Assert;
044import org.junit.BeforeClass;
045import org.junit.ClassRule;
046import org.junit.Test;
047import org.junit.experimental.categories.Category;
048
049@Category({ MediumTests.class, MasterTests.class })
050public class TestExpiredMobFileCleanerChore {
051
052  @ClassRule
053  public static final HBaseClassTestRule CLASS_RULE =
054    HBaseClassTestRule.forClass(TestExpiredMobFileCleanerChore.class);
055  private final static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
056  private final static TableName tableName = TableName.valueOf("TestExpiredMobFileCleaner");
057  private final static TableName tableName2 = TableName.valueOf("TestExpiredMobFileCleaner2");
058  private final static String family = "family";
059  private final static byte[] row1 = Bytes.toBytes("row1");
060  private final static byte[] row2 = Bytes.toBytes("row2");
061  private final static byte[] row3 = Bytes.toBytes("row3");
062  private final static byte[] qf = Bytes.toBytes("qf");
063
064  private static BufferedMutator table;
065  private static Admin admin;
066  private static BufferedMutator table2;
067  private static MobFileCleanerChore mobFileCleanerChore;
068
069  @BeforeClass
070  public static void setUp() throws Exception {
071    TEST_UTIL.getConfiguration().setInt("hfile.format.version", 3);
072    TEST_UTIL.getConfiguration().setInt(MOB_CLEANER_BATCH_SIZE_UPPER_BOUND, 2);
073    TEST_UTIL.startMiniCluster(1);
074    mobFileCleanerChore = TEST_UTIL.getMiniHBaseCluster().getMaster().getMobFileCleanerChore();
075  }
076
077  @After
078  public void cleanUp() throws IOException {
079    admin.disableTable(tableName);
080    admin.deleteTable(tableName);
081    admin.disableTable(tableName2);
082    admin.deleteTable(tableName2);
083    admin.close();
084  }
085
086  @AfterClass
087  public static void tearDown() throws Exception {
088    TEST_UTIL.shutdownMiniCluster();
089    TEST_UTIL.getTestFileSystem().delete(TEST_UTIL.getDataTestDir(), true);
090  }
091
092  @Test
093  public void testCleanerSingleThread() throws Exception {
094    TEST_UTIL.getConfiguration().setInt(MOB_CLEANER_THREAD_COUNT, 1);
095    mobFileCleanerChore.onConfigurationChange(TEST_UTIL.getConfiguration());
096    int corePoolSize = mobFileCleanerChore.getExecutor().getCorePoolSize();
097    Assert.assertEquals(1, corePoolSize);
098    testCleanerInternal();
099  }
100
101  @Test
102  public void testCleanerMultiThread() throws Exception {
103    TEST_UTIL.getConfiguration().setInt(MOB_CLEANER_THREAD_COUNT, 2);
104    mobFileCleanerChore.onConfigurationChange(TEST_UTIL.getConfiguration());
105    int corePoolSize = mobFileCleanerChore.getExecutor().getCorePoolSize();
106    Assert.assertEquals(2, corePoolSize);
107    testCleanerInternal();
108  }
109
110  private static void init() throws Exception {
111    TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tableName);
112    ColumnFamilyDescriptor columnFamilyDescriptor =
113      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(family)).setMobEnabled(true)
114        .setMobThreshold(3L).setMaxVersions(4).build();
115    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
116
117    admin = TEST_UTIL.getAdmin();
118    admin.createTable(tableDescriptorBuilder.build());
119
120    table = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration())
121      .getBufferedMutator(tableName);
122
123    TableDescriptorBuilder tableDescriptorBuilder2 = TableDescriptorBuilder.newBuilder(tableName2);
124    ColumnFamilyDescriptor columnFamilyDescriptor2 =
125      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(family)).setMobEnabled(true)
126        .setMobThreshold(3L).setMaxVersions(4).build();
127    tableDescriptorBuilder2.setColumnFamily(columnFamilyDescriptor2);
128    admin.createTable(tableDescriptorBuilder2.build());
129
130    table2 = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration())
131      .getBufferedMutator(tableName2);
132  }
133
134  private static void modifyColumnExpiryDays(int expireDays) throws Exception {
135
136    // change ttl as expire days to make some row expired
137    int timeToLive = expireDays * secondsOfDay();
138    ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = ColumnFamilyDescriptorBuilder
139      .newBuilder(Bytes.toBytes(family)).setMobEnabled(true).setMobThreshold(3L);
140    columnFamilyDescriptorBuilder.setTimeToLive(timeToLive);
141
142    admin.modifyColumnFamily(tableName, columnFamilyDescriptorBuilder.build());
143
144    ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder2 = ColumnFamilyDescriptorBuilder
145      .newBuilder(Bytes.toBytes(family)).setMobEnabled(true).setMobThreshold(3L);
146    columnFamilyDescriptorBuilder2.setTimeToLive(timeToLive);
147
148    admin.modifyColumnFamily(tableName2, columnFamilyDescriptorBuilder2.build());
149  }
150
151  private static void putKVAndFlush(BufferedMutator table, byte[] row, byte[] value, long ts,
152    TableName tableName) throws Exception {
153
154    Put put = new Put(row, ts);
155    put.addColumn(Bytes.toBytes(family), qf, value);
156    table.mutate(put);
157
158    table.flush();
159    admin.flush(tableName);
160  }
161
162  /**
163   * Creates a 3 day old hfile and an 1 day old hfile then sets expiry to 2 days. Verifies that the
164   * 3 day old hfile is removed but the 1 day one is still present after the expiry based cleaner is
165   * run.
166   */
167  private static void testCleanerInternal() throws Exception {
168    init();
169
170    Path mobDirPath = MobUtils.getMobFamilyPath(TEST_UTIL.getConfiguration(), tableName, family);
171
172    byte[] dummyData = makeDummyData(600);
173    long ts = EnvironmentEdgeManager.currentTime() - 3 * secondsOfDay() * 1000; // 3 days before
174    putKVAndFlush(table, row1, dummyData, ts, tableName);
175    FileStatus[] firstFiles = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath);
176    // the first mob file
177    assertEquals("Before cleanup without delay 1", 1, firstFiles.length);
178    String firstFile = firstFiles[0].getPath().getName();
179
180    // 1.5 day before
181    ts = (long) (EnvironmentEdgeManager.currentTime() - 1.5 * secondsOfDay() * 1000);
182    putKVAndFlush(table, row2, dummyData, ts, tableName);
183    FileStatus[] secondFiles = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath);
184    // now there are 2 mob files
185    assertEquals("Before cleanup without delay 2", 2, secondFiles.length);
186    String f1 = secondFiles[0].getPath().getName();
187    String f2 = secondFiles[1].getPath().getName();
188    String secondFile = f1.equals(firstFile) ? f2 : f1;
189
190    ts = EnvironmentEdgeManager.currentTime() - 4 * secondsOfDay() * 1000; // 4 days before
191    putKVAndFlush(table, row3, dummyData, ts, tableName);
192    ts = EnvironmentEdgeManager.currentTime() - 4 * secondsOfDay() * 1000; // 4 days before
193    putKVAndFlush(table, row3, dummyData, ts, tableName);
194    FileStatus[] thirdFiles = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath);
195    // now there are 4 mob files
196    assertEquals("Before cleanup without delay 3", 4, thirdFiles.length);
197
198    // modifyColumnExpiryDays(2); // ttl = 2, make the first row expired
199
200    // for table 2
201    Path mobDirPath2 = MobUtils.getMobFamilyPath(TEST_UTIL.getConfiguration(), tableName2, family);
202
203    byte[] dummyData2 = makeDummyData(600);
204
205    putKVAndFlush(table2, row1, dummyData2, ts, tableName2);
206    FileStatus[] firstFiles2 = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath2);
207    // the first mob file
208    assertEquals("Before cleanup without delay 1", 1, firstFiles2.length);
209    String firstFile2 = firstFiles2[0].getPath().getName();
210
211    // 1.5 day before
212    ts = (long) (EnvironmentEdgeManager.currentTime() - 1.5 * secondsOfDay() * 1000);
213    putKVAndFlush(table2, row2, dummyData2, ts, tableName2);
214    FileStatus[] secondFiles2 = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath2);
215    // now there are 2 mob files
216    assertEquals("Before cleanup without delay 2", 2, secondFiles2.length);
217    String f1Second = secondFiles2[0].getPath().getName();
218    String f2Second = secondFiles2[1].getPath().getName();
219    String secondFile2 = f1Second.equals(firstFile2) ? f2Second : f1Second;
220    ts = EnvironmentEdgeManager.currentTime() - 4 * secondsOfDay() * 1000; // 4 days before
221    putKVAndFlush(table2, row3, dummyData2, ts, tableName2);
222    ts = EnvironmentEdgeManager.currentTime() - 4 * secondsOfDay() * 1000; // 4 days before
223    putKVAndFlush(table2, row3, dummyData2, ts, tableName2);
224    FileStatus[] thirdFiles2 = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath2);
225    // now there are 4 mob files
226    assertEquals("Before cleanup without delay 3", 4, thirdFiles2.length);
227
228    modifyColumnExpiryDays(2); // ttl = 2, make the first row expired
229
230    // run the cleaner chore
231    mobFileCleanerChore.chore();
232
233    FileStatus[] filesAfterClean = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath);
234    String lastFile = filesAfterClean[0].getPath().getName();
235    // there are 4 mob files in total, but only 3 need to be cleaned
236    assertEquals("After cleanup without delay 1", 1, filesAfterClean.length);
237    assertEquals("After cleanup without delay 2", secondFile, lastFile);
238
239    filesAfterClean = TEST_UTIL.getTestFileSystem().listStatus(mobDirPath2);
240    lastFile = filesAfterClean[0].getPath().getName();
241    // there are 4 mob files in total, but only 3 need to be cleaned
242    assertEquals("After cleanup without delay 1", 1, filesAfterClean.length);
243    assertEquals("After cleanup without delay 2", secondFile2, lastFile);
244  }
245
246  private static int secondsOfDay() {
247    return 24 * 3600;
248  }
249
250  private static byte[] makeDummyData(int size) {
251    byte[] dummyData = new byte[size];
252    Bytes.random(dummyData);
253    return dummyData;
254  }
255}