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.mapreduce;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FileStatus;
030import org.apache.hadoop.fs.FileSystem;
031import org.apache.hadoop.fs.LocatedFileStatus;
032import org.apache.hadoop.fs.Path;
033import org.apache.hadoop.hbase.HBaseTestingUtil;
034import org.apache.hadoop.hbase.HConstants;
035import org.apache.hadoop.hbase.regionserver.HRegionServer;
036import org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL;
037import org.apache.hadoop.hbase.testclassification.MapReduceTests;
038import org.apache.hadoop.hbase.testclassification.MediumTests;
039import org.apache.hadoop.hbase.util.CommonFSUtils;
040import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
041import org.apache.hadoop.hdfs.DistributedFileSystem;
042import org.apache.hadoop.mapreduce.InputSplit;
043import org.apache.hadoop.mapreduce.Job;
044import org.apache.hadoop.mapreduce.JobContext;
045import org.apache.hadoop.mapreduce.TaskAttemptContext;
046import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
047import org.junit.jupiter.api.BeforeAll;
048import org.junit.jupiter.api.Tag;
049import org.junit.jupiter.api.Test;
050import org.mockito.Mockito;
051
052@Tag(MapReduceTests.TAG)
053@Tag(MediumTests.TAG)
054public class TestWALInputFormat {
055  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
056
057  @BeforeAll
058  public static void setupClass() throws Exception {
059    TEST_UTIL.startMiniCluster();
060    TEST_UTIL.createWALRootDir();
061  }
062
063  /**
064   * Test the primitive start/end time filtering.
065   */
066  @Test
067  public void testAddFile() {
068    List<FileStatus> lfss = new ArrayList<>();
069    // a plain FileSystem is never reported closed, so nothing is skipped on the startTime side
070    FileSystem fs = Mockito.mock(FileSystem.class);
071    LocatedFileStatus lfs = Mockito.mock(LocatedFileStatus.class);
072    long now = EnvironmentEdgeManager.currentTime();
073    Mockito.when(lfs.getPath()).thenReturn(new Path("/name." + now));
074    WALInputFormat.addFile(lfss, fs, lfs, now, now);
075    assertEquals(1, lfss.size());
076    WALInputFormat.addFile(lfss, fs, lfs, now - 1, now - 1);
077    assertEquals(1, lfss.size());
078    WALInputFormat.addFile(lfss, fs, lfs, now - 2, now - 1);
079    assertEquals(1, lfss.size());
080    WALInputFormat.addFile(lfss, fs, lfs, now - 2, now);
081    assertEquals(2, lfss.size());
082    WALInputFormat.addFile(lfss, fs, lfs, Long.MIN_VALUE, now);
083    assertEquals(3, lfss.size());
084    WALInputFormat.addFile(lfss, fs, lfs, Long.MIN_VALUE, Long.MAX_VALUE);
085    assertEquals(4, lfss.size());
086    WALInputFormat.addFile(lfss, fs, lfs, now, now + 2);
087    assertEquals(5, lfss.size());
088    // created before startTime, but it may have stayed open and collected in-range entries
089    WALInputFormat.addFile(lfss, fs, lfs, now + 1, now + 2);
090    assertEquals(6, lfss.size());
091    Mockito.when(lfs.getPath()).thenReturn(new Path("/name"));
092    WALInputFormat.addFile(lfss, fs, lfs, Long.MIN_VALUE, Long.MAX_VALUE);
093    assertEquals(7, lfss.size());
094    Mockito.when(lfs.getPath()).thenReturn(new Path("/name.123"));
095    WALInputFormat.addFile(lfss, fs, lfs, Long.MIN_VALUE, Long.MAX_VALUE);
096    assertEquals(8, lfss.size());
097    Mockito.when(lfs.getPath()).thenReturn(new Path("/name." + now + ".meta"));
098    WALInputFormat.addFile(lfss, fs, lfs, now, now);
099    assertEquals(9, lfss.size());
100  }
101
102  private static boolean isKept(FileSystem fs, long created, long mtime, long start, long end) {
103    return isKept(fs, created, mtime, mtime, start, end);
104  }
105
106  private static boolean isKept(FileSystem fs, long created, long staleMtime, long refreshedMtime,
107    long start, long end) {
108    List<FileStatus> result = new ArrayList<>();
109    Path path = new Path("/name." + created);
110    LocatedFileStatus lfs = Mockito.mock(LocatedFileStatus.class);
111    Mockito.when(lfs.getPath()).thenReturn(path);
112    Mockito.when(lfs.getModificationTime()).thenReturn(staleMtime);
113    try {
114      FileStatus refreshed = Mockito.mock(FileStatus.class);
115      Mockito.when(refreshed.getModificationTime()).thenReturn(refreshedMtime);
116      Mockito.when(fs.getFileStatus(path)).thenReturn(refreshed);
117    } catch (IOException e) {
118      throw new RuntimeException(e);
119    }
120    WALInputFormat.addFile(result, fs, lfs, start, end);
121    return !result.isEmpty();
122  }
123
124  /**
125   * The name of a WAL carries its creation time, which only bounds its entries from below. A WAL
126   * stays open until it rolls, so one created before startTime can still hold entries in range.
127   * Only a closed file has a final modification time that can rule it out.
128   */
129  @Test
130  public void testAddFileUsesModificationTimeOfClosedFilesOnly() throws Exception {
131    long now = EnvironmentEdgeManager.currentTime();
132
133    DistributedFileSystem closed = Mockito.mock(DistributedFileSystem.class);
134    Mockito.when(closed.isFileClosed(Mockito.any())).thenReturn(true);
135    DistributedFileSystem open = Mockito.mock(DistributedFileSystem.class);
136    Mockito.when(open.isFileClosed(Mockito.any())).thenReturn(false);
137
138    // Closed, and its last write predates the window: nothing in it can be in range.
139    assertFalse(isKept(closed, now - 100, now - 50, now, now + 100));
140
141    // Same file, but the window opens before it was closed, so it spans the boundary.
142    assertTrue(isKept(closed, now - 100, now - 50, now - 60, now + 100));
143
144    // Still open. Its mtime is stuck near the creation time and says nothing about the entries
145    // it may yet receive, so it has to be kept even though mtime is before the window.
146    assertTrue(isKept(open, now - 100, now - 100, now, now + 100));
147
148    // Created after the window closed: every entry in it is later still.
149    assertFalse(isKept(closed, now + 200, now + 200, now, now + 100));
150
151    // Race condition: file closed between listLocatedStatus and isFileClosed. The stale mtime
152    // from the listing predates the window, but the refreshed mtime (after close) does not.
153    assertTrue(isKept(closed, now - 100, now - 50, now + 10, now, now + 100));
154
155    // ViewDistributedFileSystem wrapping non-HDFS storage: isFileClosed throws
156    // UnsupportedOperationException. The file must be kept (same as non-HDFS).
157    DistributedFileSystem unsupported = Mockito.mock(DistributedFileSystem.class);
158    Mockito.when(unsupported.isFileClosed(Mockito.any()))
159      .thenThrow(new UnsupportedOperationException("mounted fs"));
160    assertTrue(isKept(unsupported, now - 100, now - 50, now, now + 100));
161  }
162
163  @Test
164  public void testHandlesArchivedWALFiles() throws Exception {
165    Configuration conf = TEST_UTIL.getConfiguration();
166    JobContext ctx = Mockito.mock(JobContext.class);
167    Mockito.when(ctx.getConfiguration()).thenReturn(conf);
168    Job job = Job.getInstance(conf);
169    TableMapReduceUtil.initCredentialsForCluster(job, conf);
170    Mockito.when(ctx.getCredentials()).thenReturn(job.getCredentials());
171
172    // Setup WAL file, then archive it
173    HRegionServer rs = TEST_UTIL.getHBaseCluster().getRegionServer(0);
174    AbstractFSWAL wal = (AbstractFSWAL) rs.getWALs().get(0);
175    Path walPath = wal.getCurrentFileName();
176    TEST_UTIL.getConfiguration().set(FileInputFormat.INPUT_DIR, walPath.toString());
177    TEST_UTIL.getConfiguration().set(WALPlayer.INPUT_FILES_SEPARATOR_KEY, ";");
178
179    Path rootDir = CommonFSUtils.getWALRootDir(conf);
180    Path archiveWal = new Path(rootDir, HConstants.HREGION_OLDLOGDIR_NAME);
181    archiveWal = new Path(archiveWal, walPath.getName());
182    TEST_UTIL.getTestFileSystem().delete(walPath, true);
183    TEST_UTIL.getTestFileSystem().mkdirs(archiveWal.getParent());
184    TEST_UTIL.getTestFileSystem().create(archiveWal).close();
185
186    // Test for that we can read from the archived WAL file
187    WALInputFormat wif = new WALInputFormat();
188    List<InputSplit> splits = wif.getSplits(ctx);
189    assertEquals(1, splits.size());
190    WALInputFormat.WALSplit split = (WALInputFormat.WALSplit) splits.get(0);
191    assertEquals(archiveWal.toString(), split.getLogFileName());
192  }
193
194  @Test
195  public void testEmptyFileIsIgnoredWhenConfigured() throws IOException, InterruptedException {
196    List<InputSplit> splits = getSplitsForEmptyFile(true);
197    assertTrue(splits.isEmpty(), "Empty file should be ignored when IGNORE_EMPTY_FILES is true");
198  }
199
200  @Test
201  public void testEmptyFileIsIncludedWhenNotIgnored() throws IOException, InterruptedException {
202    List<InputSplit> splits = getSplitsForEmptyFile(false);
203    assertEquals(1, splits.size(),
204      "Empty file should be included when IGNORE_EMPTY_FILES is false");
205  }
206
207  private List<InputSplit> getSplitsForEmptyFile(boolean ignoreEmptyFiles)
208    throws IOException, InterruptedException {
209    Configuration conf = new Configuration();
210    conf.setBoolean(WALPlayer.IGNORE_EMPTY_FILES, ignoreEmptyFiles);
211
212    JobContext jobContext = Mockito.mock(JobContext.class);
213    Mockito.when(jobContext.getConfiguration()).thenReturn(conf);
214
215    LocatedFileStatus emptyFile = Mockito.mock(LocatedFileStatus.class);
216    Mockito.when(emptyFile.getLen()).thenReturn(0L);
217    Mockito.when(emptyFile.getPath()).thenReturn(new Path("/empty.wal"));
218
219    WALInputFormat inputFormat = new WALInputFormat() {
220      @Override
221      Path[] getInputPaths(Configuration conf) {
222        return new Path[] { new Path("/input") };
223      }
224
225      @Override
226      List<FileStatus> getFiles(FileSystem fs, Path inputPath, long startTime, long endTime,
227        Configuration conf) {
228        return Collections.singletonList(emptyFile);
229      }
230    };
231
232    return inputFormat.getSplits(jobContext, "", "");
233  }
234
235  /**
236   * Test that an empty WAL file (which causes WALHeaderEOFException) is gracefully handled and
237   * skipped rather than causing the job to fail.
238   */
239  @Test
240  public void testHandlesEmptyWALFile() throws Exception {
241    Configuration conf = TEST_UTIL.getConfiguration();
242
243    // Create an empty WAL file
244    Path walRootDir = CommonFSUtils.getWALRootDir(conf);
245    Path emptyWalFile =
246      new Path(walRootDir, "WALs/empty-wal-test/empty." + EnvironmentEdgeManager.currentTime());
247    TEST_UTIL.getTestFileSystem().mkdirs(emptyWalFile.getParent());
248    TEST_UTIL.getTestFileSystem().create(emptyWalFile).close();
249
250    try {
251      JobContext ctx = Mockito.mock(JobContext.class);
252      conf.set(FileInputFormat.INPUT_DIR, emptyWalFile.toString());
253      conf.set(WALPlayer.INPUT_FILES_SEPARATOR_KEY, ";");
254      Mockito.when(ctx.getConfiguration()).thenReturn(conf);
255      Job job = Job.getInstance(conf);
256      TableMapReduceUtil.initCredentialsForCluster(job, conf);
257      Mockito.when(ctx.getCredentials()).thenReturn(job.getCredentials());
258
259      // Create record reader and verify it handles the empty file gracefully
260      try (WALInputFormat.WALKeyRecordReader reader = new WALInputFormat.WALKeyRecordReader()) {
261        TaskAttemptContext taskCtx = Mockito.mock(TaskAttemptContext.class);
262        Mockito.when(taskCtx.getConfiguration()).thenReturn(conf);
263
264        WALInputFormat wif = new WALInputFormat();
265        List<InputSplit> splits = wif.getSplits(ctx);
266        assertEquals(1, splits.size());
267        WALInputFormat.WALSplit split = (WALInputFormat.WALSplit) splits.get(0);
268
269        // This should not throw WALHeaderEOFException - it should return false for nextKeyValue()
270        reader.initialize(split, taskCtx);
271        // nextKeyValue() should return false since the file is empty (reader is null)
272        assertFalse(reader.nextKeyValue());
273      }
274    } finally {
275      TEST_UTIL.getTestFileSystem().delete(emptyWalFile.getParent(), true);
276    }
277  }
278
279}