diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java index 11e22efac97a..a1ec67345b5e 100644 --- a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java +++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java @@ -29,6 +29,7 @@ import java.net.URLDecoder; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -1047,6 +1048,7 @@ public static List getValidWalDirs(Configuration conf, Path walBackupDir List validDirs = new ArrayList<>(); SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); + dateFormat.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); for (FileStatus dayDir : dayDirs) { if (!dayDir.isDirectory()) { diff --git a/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupUtils.java b/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupUtils.java index 3fc2c31a9d51..bd5cb8358d3d 100644 --- a/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupUtils.java +++ b/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupUtils.java @@ -17,8 +17,17 @@ */ package org.apache.hadoop.hbase.backup; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import java.io.IOException; import java.security.PrivilegedAction; +import java.time.Instant; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; @@ -34,6 +43,7 @@ import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.security.UserGroupInformation; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -50,6 +60,15 @@ public class TestBackupUtils { protected static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); protected static Configuration conf = TEST_UTIL.getConfiguration(); + private static FileSystem dummyFs; + private static Path backupRootDir; + + @BeforeClass + public static void setUp() throws IOException { + dummyFs = TEST_UTIL.getTestFileSystem(); + backupRootDir = TEST_UTIL.getDataTestDirOnTestFS("backupUT"); + } + @Test public void testGetBulkOutputDir() { // Create a user who is not the current user @@ -82,7 +101,7 @@ public Path run() { } }); // Make sure the directory is in foo1234's home directory - Assert.assertTrue(bulkOutputDir.toString().startsWith(fooHomeDirectory.toString())); + assertTrue(bulkOutputDir.toString().startsWith(fooHomeDirectory.toString())); } @Test @@ -99,7 +118,7 @@ public void testFilesystemWalHostNameParsing() throws IOException { Path testOldWalPath = new Path(oldLogDir, serverName + BackupUtils.LOGNAME_SEPARATOR + EnvironmentEdgeManager.currentTime()); - Assert.assertEquals(host + Addressing.HOSTNAME_PORT_SEPARATOR + port, + assertEquals(host + Addressing.HOSTNAME_PORT_SEPARATOR + port, BackupUtils.parseHostFromOldLog(testOldWalPath)); Path testMasterWalPath = @@ -110,9 +129,99 @@ public void testFilesystemWalHostNameParsing() throws IOException { Path testOldWalWithRegionGroupingPath = new Path(oldLogDir, serverName + BackupUtils.LOGNAME_SEPARATOR + serverName + BackupUtils.LOGNAME_SEPARATOR + "regiongroup-0" + BackupUtils.LOGNAME_SEPARATOR + EnvironmentEdgeManager.currentTime()); - Assert.assertEquals(host + Addressing.HOSTNAME_PORT_SEPARATOR + port, + assertEquals(host + Addressing.HOSTNAME_PORT_SEPARATOR + port, BackupUtils.parseHostFromOldLog(testOldWalWithRegionGroupingPath)); } + } + + // Ensure getValidWalDirs() uses UTC timestamps regardless of what time zone the test is run in. + @Test + public void testGetValidWalDirForAllTimeZonesSingleDay() throws IOException { + // This UTC test time is a time when it is still "yesterday" in other time zones (such as PST) + List walDateDirs = List.of("2026-01-23"); + Path walDir = new Path(backupRootDir, "WALs"); + + // 10-minute window in UTC between start and end time + long startTime = Instant.parse("2026-01-23T01:00:00Z").toEpochMilli(); + long endTime = startTime + (10 * 60 * 1000); + + testGetValidWalDirs(startTime, endTime, walDir, walDateDirs, 1, walDateDirs); + } + + // Ensure getValidWalDirs() works as expected for time ranges across multiple days for all time + // zones + @Test + public void testGetValidWalDirsForAllTimeZonesMultiDay() throws IOException { + List walDateDirs = List.of("2025-12-30", "2025-12-31", "2026-01-01", "2026-01-02"); + List expectedValidWalDirs = List.of("2025-12-31", "2026-01-01"); + Path walDir = new Path(backupRootDir, "WALs"); + + // 10-minute window in UTC between start and end time that spans over two days + long startTime = Instant.parse("2025-12-31T23:55:00Z").toEpochMilli(); + long endTime = Instant.parse("2026-01-01T00:05:00Z").toEpochMilli(); + + testGetValidWalDirs(startTime, endTime, walDir, walDateDirs, 2, expectedValidWalDirs); + } + + @Test + public void testGetValidWalDirExactlyMidnightUTC() throws IOException { + List walDateDirs = List.of("2026-01-23"); + Path walDir = new Path(backupRootDir, "WALs"); + // This instant is UTC + long startAndEndTime = Instant.parse("2026-01-23T00:00:00.000Z").toEpochMilli(); + testGetValidWalDirs(startAndEndTime, startAndEndTime, walDir, walDateDirs, 1, walDateDirs); + } + + @Test + public void testGetValidWalDirOneMsBeforeMidnightUTC() throws IOException { + List walDateDirs = List.of("2026-01-23"); + Path walDir = new Path(backupRootDir, "WALs"); + // This instant is UTC + long startAndEndTime = Instant.parse("2026-01-23T23:59:59.999Z").toEpochMilli(); + + testGetValidWalDirs(startAndEndTime, startAndEndTime, walDir, walDateDirs, 1, walDateDirs); + } + + protected void testGetValidWalDirs(long startTime, long endTime, Path walDir, + List availableWalDateDirs, int numExpectedValidWalDirs, + List expectedValidWalDirs) throws IOException { + TimeZone defaultTimeZone = TimeZone.getDefault(); + try { + // This UTC test time is a time when it is still "yesterday" in other time zones (such as PST) + for (String dirName : availableWalDateDirs) { + dummyFs.mkdirs(new Path(walDir, dirName)); + } + + // Ensure we can get valid WAL dirs regardless of the test environment's time zone + for (String timeZone : ZoneId.getAvailableZoneIds()) { + // Force test environment to use specified time zone + TimeZone.setDefault(TimeZone.getTimeZone(timeZone)); + + List validWalDirs = BackupUtils.getValidWalDirs(TEST_UTIL.getConfiguration(), + backupRootDir, startTime, endTime); + + // Verify the correct number of valid WAL dirs was found + assertEquals("The number of valid WAL dirs should be " + numExpectedValidWalDirs + + " for time zone " + timeZone, numExpectedValidWalDirs, validWalDirs.size()); + + // Verify the list of valid WAL dirs is as expected + for (String dirName : expectedValidWalDirs) { + assertTrue("Expected " + dirName + " to be a valid WAL dir", + validWalDirs.stream().anyMatch(path -> path.endsWith("/" + dirName))); + } + + // Verify the list of valid WAL dirs does not contain anything expected to be invalid + List expectedInvalidWalDirs = new ArrayList<>(availableWalDateDirs); + expectedInvalidWalDirs.removeAll(expectedValidWalDirs); + for (String dirName : expectedInvalidWalDirs) { + assertFalse("Expected " + dirName + " to NOT be a valid WAL dir", + validWalDirs.contains(dirName)); + } + } + } finally { + TimeZone.setDefault(defaultTimeZone); + dummyFs.delete(walDir, true); + } } }