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.backup;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertNull;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.IOException;
026import java.security.PrivilegedAction;
027import java.time.Instant;
028import java.time.ZoneId;
029import java.util.ArrayList;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033import java.util.TimeZone;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.hadoop.fs.FileSystem;
036import org.apache.hadoop.fs.Path;
037import org.apache.hadoop.hbase.HBaseTestingUtil;
038import org.apache.hadoop.hbase.HConstants;
039import org.apache.hadoop.hbase.ServerName;
040import org.apache.hadoop.hbase.backup.util.BackupUtils;
041import org.apache.hadoop.hbase.master.region.MasterRegionFactory;
042import org.apache.hadoop.hbase.testclassification.SmallTests;
043import org.apache.hadoop.hbase.util.Addressing;
044import org.apache.hadoop.hbase.util.CommonFSUtils;
045import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
046import org.apache.hadoop.security.UserGroupInformation;
047import org.junit.jupiter.api.BeforeAll;
048import org.junit.jupiter.api.Tag;
049import org.junit.jupiter.api.Test;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053@Tag(SmallTests.TAG)
054public class TestBackupUtils {
055
056  private static final Logger LOG = LoggerFactory.getLogger(TestBackupUtils.class);
057
058  protected static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
059  protected static Configuration conf = TEST_UTIL.getConfiguration();
060
061  private static FileSystem dummyFs;
062  private static Path backupRootDir;
063
064  @BeforeAll
065  public static void setUp() throws IOException {
066    dummyFs = TEST_UTIL.getTestFileSystem();
067    backupRootDir = TEST_UTIL.getDataTestDirOnTestFS("backupUT");
068  }
069
070  @Test
071  public void testGetBulkOutputDir() {
072    // Create a user who is not the current user
073    String fooUserName = "foo1234";
074    String fooGroupName = "group1";
075    UserGroupInformation ugi =
076      UserGroupInformation.createUserForTesting(fooUserName, new String[] { fooGroupName });
077    // Get user's home directory
078    Path fooHomeDirectory = ugi.doAs(new PrivilegedAction<Path>() {
079      @Override
080      public Path run() {
081        try (FileSystem fs = FileSystem.get(conf)) {
082          return fs.getHomeDirectory();
083        } catch (IOException ioe) {
084          LOG.error("Failed to get foo's home directory", ioe);
085        }
086        return null;
087      }
088    });
089
090    Path bulkOutputDir = ugi.doAs(new PrivilegedAction<Path>() {
091      @Override
092      public Path run() {
093        try {
094          return BackupUtils.getBulkOutputDir("test", conf, false);
095        } catch (IOException ioe) {
096          LOG.error("Failed to get bulk output dir path", ioe);
097        }
098        return null;
099      }
100    });
101    // Make sure the directory is in foo1234's home directory
102    assertTrue(bulkOutputDir.toString().startsWith(fooHomeDirectory.toString()));
103  }
104
105  @Test
106  public void testFilesystemWalHostNameParsing() throws IOException {
107    String[] hosts =
108      new String[] { "10.20.30.40", "127.0.0.1", "localhost", "a-region-server.domain.com" };
109
110    Path walRootDir = CommonFSUtils.getWALRootDir(conf);
111    Path oldLogDir = new Path(walRootDir, HConstants.HREGION_OLDLOGDIR_NAME);
112
113    int port = 60030;
114    for (String host : hosts) {
115      ServerName serverName = ServerName.valueOf(host, port, 1234);
116
117      Path testOldWalPath = new Path(oldLogDir,
118        serverName + BackupUtils.LOGNAME_SEPARATOR + EnvironmentEdgeManager.currentTime());
119      assertEquals(host + Addressing.HOSTNAME_PORT_SEPARATOR + port,
120        BackupUtils.parseHostFromOldLog(testOldWalPath));
121
122      Path testMasterWalPath =
123        new Path(oldLogDir, testOldWalPath.getName() + MasterRegionFactory.ARCHIVED_WAL_SUFFIX);
124      assertNull(BackupUtils.parseHostFromOldLog(testMasterWalPath));
125
126      // org.apache.hadoop.hbase.wal.BoundedGroupingStrategy does this
127      Path testOldWalWithRegionGroupingPath = new Path(oldLogDir,
128        serverName + BackupUtils.LOGNAME_SEPARATOR + serverName + BackupUtils.LOGNAME_SEPARATOR
129          + "regiongroup-0" + BackupUtils.LOGNAME_SEPARATOR + EnvironmentEdgeManager.currentTime());
130      assertEquals(host + Addressing.HOSTNAME_PORT_SEPARATOR + port,
131        BackupUtils.parseHostFromOldLog(testOldWalWithRegionGroupingPath));
132
133      Path testOldWalInServerDirPath = new Path(new Path(oldLogDir, serverName.toString()),
134        "wal" + BackupUtils.LOGNAME_SEPARATOR + EnvironmentEdgeManager.currentTime());
135      assertEquals(host + Addressing.HOSTNAME_PORT_SEPARATOR + port,
136        BackupUtils.parseHostFromOldLog(testOldWalInServerDirPath));
137    }
138  }
139
140  // Ensure getValidWalDirs() uses UTC timestamps regardless of what time zone the test is run in.
141  @Test
142  public void testGetValidWalDirForAllTimeZonesSingleDay() throws IOException {
143    // This UTC test time is a time when it is still "yesterday" in other time zones (such as PST)
144    List<String> walDateDirs = List.of("2026-01-23");
145    Path walDir = new Path(backupRootDir, "WALs");
146
147    // 10-minute window in UTC between start and end time
148    long startTime = Instant.parse("2026-01-23T01:00:00Z").toEpochMilli();
149    long endTime = startTime + (10 * 60 * 1000);
150
151    testGetValidWalDirs(startTime, endTime, walDir, walDateDirs, 1, walDateDirs);
152  }
153
154  // Ensure getValidWalDirs() works as expected for time ranges across multiple days for all time
155  // zones
156  @Test
157  public void testGetValidWalDirsForAllTimeZonesMultiDay() throws IOException {
158    List<String> walDateDirs = List.of("2025-12-30", "2025-12-31", "2026-01-01", "2026-01-02");
159    List<String> expectedValidWalDirs = List.of("2025-12-31", "2026-01-01");
160    Path walDir = new Path(backupRootDir, "WALs");
161
162    // 10-minute window in UTC between start and end time that spans over two days
163    long startTime = Instant.parse("2025-12-31T23:55:00Z").toEpochMilli();
164    long endTime = Instant.parse("2026-01-01T00:05:00Z").toEpochMilli();
165
166    testGetValidWalDirs(startTime, endTime, walDir, walDateDirs, 2, expectedValidWalDirs);
167  }
168
169  @Test
170  public void testGetValidWalDirExactlyMidnightUTC() throws IOException {
171    List<String> walDateDirs = List.of("2026-01-23");
172    Path walDir = new Path(backupRootDir, "WALs");
173    // This instant is UTC
174    long startAndEndTime = Instant.parse("2026-01-23T00:00:00.000Z").toEpochMilli();
175
176    testGetValidWalDirs(startAndEndTime, startAndEndTime, walDir, walDateDirs, 1, walDateDirs);
177  }
178
179  @Test
180  public void testGetValidWalDirOneMsBeforeMidnightUTC() throws IOException {
181    List<String> walDateDirs = List.of("2026-01-23");
182    Path walDir = new Path(backupRootDir, "WALs");
183    // This instant is UTC
184    long startAndEndTime = Instant.parse("2026-01-23T23:59:59.999Z").toEpochMilli();
185
186    testGetValidWalDirs(startAndEndTime, startAndEndTime, walDir, walDateDirs, 1, walDateDirs);
187  }
188
189  @Test
190  public void testGetRolledHostsKeepsOnlyHostsWhoseRollResultChanged() {
191    Map<String, Long> previousLogRolls =
192      Map.of("rolled:16020", 100L, "offline:16020", 200L, "removed:16020", 300L);
193    Map<String, Long> latestLogRolls =
194      Map.of("rolled:16020", 150L, "offline:16020", 200L, "new:16020", 400L);
195
196    assertEquals(Map.of("rolled:16020", 150L, "new:16020", 400L),
197      BackupUtils.getRolledHosts(previousLogRolls, latestLogRolls));
198  }
199
200  @Test
201  public void testComputeLogBoundariesUsesRollResultForRolledHosts() throws IOException {
202    Map<String, Long> rolledHosts = Map.of("rolled:16020", 100L);
203    List<String> coveredLogs = List.of("/hbase/oldWALs/rolled%2C16020%2C1.500");
204    List<String> pendingLogs = List.of("/hbase/WALs/rolled,16020,1/rolled%2C16020%2C1.50");
205
206    assertEquals(Map.of("rolled:16020", 100L),
207      BackupUtils.computeLogBoundaries(rolledHosts, coveredLogs, pendingLogs));
208  }
209
210  @Test
211  public void testComputeLogBoundariesUsesNewestCoveredLogForOtherHosts() throws IOException {
212    List<String> coveredLogs =
213      List.of("/hbase/oldWALs/offline%2C16020%2C1.200", "/hbase/oldWALs/offline%2C16020%2C1.300",
214        "/hbase/WALs/offline,16020,1/offline%2C16020%2C1.250");
215
216    assertEquals(Map.of("offline:16020", 300L),
217      BackupUtils.computeLogBoundaries(Map.of(), coveredLogs, List.of()));
218  }
219
220  @Test
221  public void testComputeLogBoundariesCapsBelowOldestPendingLog() throws IOException {
222    List<String> coveredLogs = List.of("/hbase/oldWALs/splitting%2C16020%2C1.400");
223    List<String> pendingLogs =
224      List.of("/hbase/WALs/splitting,16020,1-splitting/splitting%2C16020%2C1.350",
225        "/hbase/WALs/splitting,16020,1-splitting/splitting%2C16020%2C1.370");
226
227    assertEquals(Map.of("splitting:16020", 349L),
228      BackupUtils.computeLogBoundaries(Map.of(), coveredLogs, pendingLogs));
229  }
230
231  @Test
232  public void testComputeLogBoundariesKeepsHostWithOnlyPendingLogs() throws IOException {
233    List<String> pendingLogs = List.of("/hbase/WALs/joined,16020,1/joined%2C16020%2C1.500");
234
235    assertEquals(Map.of("joined:16020", 499L),
236      BackupUtils.computeLogBoundaries(Map.of(), List.of(), pendingLogs));
237  }
238
239  @Test
240  public void testComputeLogBoundariesSkipsUnparseableLogs() throws IOException {
241    List<String> coveredLogs = List.of("/hbase/oldWALs/not-a-wal");
242
243    assertEquals(Map.of("rolled:16020", 100L),
244      BackupUtils.computeLogBoundaries(Map.of("rolled:16020", 100L), coveredLogs, List.of()));
245  }
246
247  @Test
248  public void testComputeLogBoundariesKeepsPreviousBoundaryWhileHostHasLogs() throws IOException {
249    Map<String, Long> previousBoundaries = Map.of("stuck:16020", 1200L, "gone:16020", 800L);
250
251    assertEquals(Map.of("stuck:16020", 1200L), BackupUtils.computeLogBoundaries(Map.of(),
252      previousBoundaries, Set.of("stuck:16020"), List.of(), List.of()));
253  }
254
255  @Test
256  public void testComputeLogBoundariesPrefersNewBoundaryOverPreviousOne() throws IOException {
257    Map<String, Long> previousBoundaries =
258      Map.of("rolled:16020", 100L, "offline:16020", 200L, "pending:16020", 300L);
259    List<String> coveredLogs = List.of("/hbase/oldWALs/offline%2C16020%2C1.250");
260    List<String> pendingLogs = List.of("/hbase/WALs/pending,16020,1/pending%2C16020%2C1.400");
261
262    assertEquals(Map.of("rolled:16020", 150L, "offline:16020", 250L, "pending:16020", 399L),
263      BackupUtils.computeLogBoundaries(Map.of("rolled:16020", 150L), previousBoundaries,
264        Set.of("rolled:16020", "offline:16020", "pending:16020"), coveredLogs, pendingLogs));
265  }
266
267  protected void testGetValidWalDirs(long startTime, long endTime, Path walDir,
268    List<String> availableWalDateDirs, int numExpectedValidWalDirs,
269    List<String> expectedValidWalDirs) throws IOException {
270    TimeZone defaultTimeZone = TimeZone.getDefault();
271    try {
272      // This UTC test time is a time when it is still "yesterday" in other time zones (such as PST)
273      for (String dirName : availableWalDateDirs) {
274        dummyFs.mkdirs(new Path(walDir, dirName));
275      }
276
277      // Ensure we can get valid WAL dirs regardless of the test environment's time zone
278      for (String timeZone : ZoneId.getAvailableZoneIds()) {
279        // Force test environment to use specified time zone
280        TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
281
282        List<String> validWalDirs = BackupUtils.getValidWalDirs(TEST_UTIL.getConfiguration(),
283          backupRootDir, startTime, endTime);
284
285        // Verify the correct number of valid WAL dirs was found
286        assertEquals(numExpectedValidWalDirs, validWalDirs.size(),
287          "The number of valid WAL dirs should be " + numExpectedValidWalDirs + " for time zone "
288            + timeZone);
289
290        // Verify the list of valid WAL dirs is as expected
291        for (String dirName : expectedValidWalDirs) {
292          assertTrue(validWalDirs.stream().anyMatch(path -> path.endsWith("/" + dirName)),
293            "Expected " + dirName + " to be a valid WAL dir");
294        }
295
296        // Verify the list of valid WAL dirs does not contain anything expected to be invalid
297        List<String> expectedInvalidWalDirs = new ArrayList<>(availableWalDateDirs);
298        expectedInvalidWalDirs.removeAll(expectedValidWalDirs);
299        for (String dirName : expectedInvalidWalDirs) {
300          assertFalse(validWalDirs.contains(dirName),
301            "Expected " + dirName + " to NOT be a valid WAL dir");
302        }
303      }
304    } finally {
305      TimeZone.setDefault(defaultTimeZone);
306      dummyFs.delete(walDir, true);
307    }
308  }
309}