This is an automated email from the ASF dual-hosted git repository.

andor pushed a commit to branch HBASE-28957_rebase
in repository https://gitbox.apache.org/repos/asf/hbase.git

commit 54dbbbb59f0333a6248a708e482da7e2e3a045c0
Author: vinayak hegde <vinayakph...@gmail.com>
AuthorDate: Thu Apr 10 16:35:02 2025 +0530

    HBASE-29210: Introduce Validation for PITR-Critical Backup Deletion (#6848)
    
    Signed-off-by: Andor Molnár <an...@apache.org>
    Signed-off-by: Wellington Chevreuil <wchevre...@apache.org>
---
 .../apache/hadoop/hbase/backup/BackupDriver.java   |   4 +
 .../hbase/backup/BackupRestoreConstants.java       |   8 +
 .../hadoop/hbase/backup/impl/BackupCommands.java   | 173 ++++++++++++++++++-
 .../hadoop/hbase/backup/TestBackupDelete.java      |   6 +-
 ...estBackupDeleteWithContinuousBackupAndPITR.java | 186 +++++++++++++++++++++
 5 files changed, 369 insertions(+), 8 deletions(-)

diff --git 
a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupDriver.java 
b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupDriver.java
index e096bbee161..eb27e9a60e0 100644
--- 
a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupDriver.java
+++ 
b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupDriver.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.hbase.backup;
 
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.LONG_OPTION_ENABLE_CONTINUOUS_BACKUP;
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.LONG_OPTION_FORCE_DELETE;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BACKUP_LIST_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH_DESC;
@@ -25,6 +26,8 @@ import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_ENABLE_CONTINUOUS_BACKUP;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_ENABLE_CONTINUOUS_BACKUP_DESC;
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_FORCE_DELETE;
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_FORCE_DELETE_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_IGNORECHECKSUM;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_IGNORECHECKSUM_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP;
@@ -164,6 +167,7 @@ public class BackupDriver extends AbstractHBaseTool {
     addOptWithArg(OPTION_YARN_QUEUE_NAME, OPTION_YARN_QUEUE_NAME_DESC);
     addOptNoArg(OPTION_ENABLE_CONTINUOUS_BACKUP, 
LONG_OPTION_ENABLE_CONTINUOUS_BACKUP,
       OPTION_ENABLE_CONTINUOUS_BACKUP_DESC);
+    addOptNoArg(OPTION_FORCE_DELETE, LONG_OPTION_FORCE_DELETE, 
OPTION_FORCE_DELETE_DESC);
   }
 
   @Override
diff --git 
a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreConstants.java
 
b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreConstants.java
index f5c49adb696..3df67ac1aef 100644
--- 
a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreConstants.java
+++ 
b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreConstants.java
@@ -104,6 +104,11 @@ public interface BackupRestoreConstants {
   String OPTION_ENABLE_CONTINUOUS_BACKUP_DESC =
     "Flag indicating that the full backup is part of a continuous backup 
process.";
 
+  String OPTION_FORCE_DELETE = "fd";
+  String LONG_OPTION_FORCE_DELETE = "force-delete";
+  String OPTION_FORCE_DELETE_DESC =
+    "Flag to forcefully delete the backup, even if it may be required for 
Point-in-Time Restore";
+
   String JOB_NAME_CONF_KEY = "mapreduce.job.name";
 
   String BACKUP_CONFIG_STRING = BackupRestoreConstants.BACKUP_ENABLE_KEY + 
"=true\n"
@@ -138,6 +143,9 @@ public interface BackupRestoreConstants {
 
   String CONF_CONTINUOUS_BACKUP_WAL_DIR = "hbase.backup.continuous.wal.dir";
 
+  String CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS = 
"hbase.backup.continuous.pitr.window.days";
+  long DEFAULT_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS = 30;
+
   enum BackupCommand {
     CREATE,
     CANCEL,
diff --git 
a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupCommands.java
 
b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupCommands.java
index ab9ca1c4ed2..e9d14d1426d 100644
--- 
a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupCommands.java
+++ 
b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupCommands.java
@@ -17,6 +17,8 @@
  */
 package org.apache.hadoop.hbase.backup.impl;
 
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS;
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.DEFAULT_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BACKUP_LIST_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH_DESC;
@@ -24,6 +26,8 @@ import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_ENABLE_CONTINUOUS_BACKUP;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_ENABLE_CONTINUOUS_BACKUP_DESC;
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_FORCE_DELETE;
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_FORCE_DELETE_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_IGNORECHECKSUM;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_IGNORECHECKSUM_DESC;
 import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP;
@@ -46,8 +50,12 @@ import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_
 
 import java.io.IOException;
 import java.net.URI;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.agrona.collections.MutableLong;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.conf.Configured;
@@ -632,15 +640,18 @@ public final class BackupCommands {
         printUsage();
         throw new IOException(INCORRECT_USAGE);
       }
+
+      boolean isForceDelete = cmdline.hasOption(OPTION_FORCE_DELETE);
       super.execute();
       if (cmdline.hasOption(OPTION_KEEP)) {
-        executeDeleteOlderThan(cmdline);
+        executeDeleteOlderThan(cmdline, isForceDelete);
       } else if (cmdline.hasOption(OPTION_LIST)) {
-        executeDeleteListOfBackups(cmdline);
+        executeDeleteListOfBackups(cmdline, isForceDelete);
       }
     }
 
-    private void executeDeleteOlderThan(CommandLine cmdline) throws 
IOException {
+    private void executeDeleteOlderThan(CommandLine cmdline, boolean 
isForceDelete)
+      throws IOException {
       String value = cmdline.getOptionValue(OPTION_KEEP);
       int days = 0;
       try {
@@ -662,6 +673,7 @@ public final class BackupCommands {
         BackupAdminImpl admin = new BackupAdminImpl(conn)) {
         history = sysTable.getBackupHistory(-1, dateFilter);
         String[] backupIds = convertToBackupIds(history);
+        validatePITRBackupDeletion(backupIds, isForceDelete);
         int deleted = admin.deleteBackups(backupIds);
         System.out.println("Deleted " + deleted + " backups. Total older than 
" + days + " days: "
           + backupIds.length);
@@ -680,10 +692,11 @@ public final class BackupCommands {
       return ids;
     }
 
-    private void executeDeleteListOfBackups(CommandLine cmdline) throws 
IOException {
+    private void executeDeleteListOfBackups(CommandLine cmdline, boolean 
isForceDelete)
+      throws IOException {
       String value = cmdline.getOptionValue(OPTION_LIST);
       String[] backupIds = value.split(",");
-
+      validatePITRBackupDeletion(backupIds, isForceDelete);
       try (BackupAdminImpl admin = new BackupAdminImpl(conn)) {
         int deleted = admin.deleteBackups(backupIds);
         System.out.println("Deleted " + deleted + " backups. Total requested: 
" + backupIds.length);
@@ -695,12 +708,162 @@ public final class BackupCommands {
 
     }
 
+    /**
+     * Validates whether the specified backups can be deleted while preserving 
Point-In-Time
+     * Recovery (PITR) capabilities. If a backup is the only remaining full 
backup enabling PITR for
+     * certain tables, deletion is prevented unless forced.
+     * @param backupIds     Array of backup IDs to validate.
+     * @param isForceDelete Flag indicating whether deletion should proceed 
regardless of PITR
+     *                      constraints.
+     * @throws IOException If a backup is essential for PITR and force 
deletion is not enabled.
+     */
+    private void validatePITRBackupDeletion(String[] backupIds, boolean 
isForceDelete)
+      throws IOException {
+      if (!isForceDelete) {
+        for (String backupId : backupIds) {
+          List<TableName> affectedTables = 
getTablesDependentOnBackupForPITR(backupId);
+          if (!affectedTables.isEmpty()) {
+            String errMsg = String.format(
+              "Backup %s is the only FULL backup remaining that enables PITR 
for tables: %s. "
+                + "Use the force option to delete it anyway.",
+              backupId, affectedTables);
+            System.err.println(errMsg);
+            throw new IOException(errMsg);
+          }
+        }
+      }
+    }
+
+    /**
+     * Identifies tables that rely on the specified backup for PITR. If a 
table has no other valid
+     * FULL backups that can facilitate recovery to all points within the PITR 
retention window, it
+     * is added to the dependent list.
+     * @param backupId The backup ID being evaluated.
+     * @return List of tables dependent on the specified backup for PITR.
+     * @throws IOException If backup metadata cannot be retrieved.
+     */
+    private List<TableName> getTablesDependentOnBackupForPITR(String backupId) 
throws IOException {
+      List<TableName> dependentTables = new ArrayList<>();
+
+      try (final BackupSystemTable backupSystemTable = new 
BackupSystemTable(conn)) {
+        BackupInfo targetBackup = backupSystemTable.readBackupInfo(backupId);
+
+        if (targetBackup == null) {
+          throw new IOException("Backup info not found for backupId: " + 
backupId);
+        }
+
+        // Only full backups are mandatory for PITR
+        if (!BackupType.FULL.equals(targetBackup.getType())) {
+          return List.of();
+        }
+
+        // Retrieve the tables with continuous backup enabled and their start 
times
+        Map<TableName, Long> continuousBackupStartTimes =
+          backupSystemTable.getContinuousBackupTableSet();
+
+        // Determine the PITR time window
+        long pitrWindowDays = 
getConf().getLong(CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS,
+          DEFAULT_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS);
+        long currentTime = EnvironmentEdgeManager.getDelegate().currentTime();
+        final MutableLong pitrMaxStartTime =
+          new MutableLong(currentTime - 
TimeUnit.DAYS.toMillis(pitrWindowDays));
+
+        // For all tables, determine the earliest (minimum) continuous backup 
start time.
+        // This represents the actual earliest point-in-time recovery (PITR) 
timestamp
+        // that can be used, ensuring we do not go beyond the available backup 
data.
+        long minContinuousBackupStartTime = currentTime;
+        for (TableName table : targetBackup.getTableNames()) {
+          minContinuousBackupStartTime = Math.min(minContinuousBackupStartTime,
+            continuousBackupStartTimes.getOrDefault(table, currentTime));
+        }
+
+        // The PITR max start time should be the maximum of the calculated 
minimum continuous backup
+        // start time and the default PITR max start time (based on the 
configured window).
+        // This ensures that PITR does not extend beyond what is practically 
possible.
+        pitrMaxStartTime.set(Math.max(minContinuousBackupStartTime, 
pitrMaxStartTime.longValue()));
+
+        for (TableName table : targetBackup.getTableNames()) {
+          // This backup is not necessary for this table since it doesn't have 
PITR enabled
+          if (!continuousBackupStartTimes.containsKey(table)) {
+            continue;
+          }
+          if (
+            !isValidPITRBackup(targetBackup, table, continuousBackupStartTimes,
+              pitrMaxStartTime.longValue())
+          ) {
+            continue; // This backup is not crucial for PITR of this table
+          }
+
+          // Check if another valid full backup exists for this table
+          List<BackupInfo> backupHistory = 
backupSystemTable.getBackupInfos(BackupState.COMPLETE);
+          boolean hasAnotherValidBackup = backupHistory.stream()
+            .anyMatch(backup -> !backup.getBackupId().equals(backupId) && 
isValidPITRBackup(backup,
+              table, continuousBackupStartTimes, 
pitrMaxStartTime.longValue()));
+
+          if (!hasAnotherValidBackup) {
+            dependentTables.add(table);
+          }
+        }
+      }
+      return dependentTables;
+    }
+
+    /**
+     * Determines if a given backup is a valid candidate for Point-In-Time 
Recovery (PITR) for a
+     * specific table. A valid backup ensures that recovery is possible to any 
point within the PITR
+     * retention window. A backup qualifies if:
+     * <ul>
+     * <li>It is a FULL backup.</li>
+     * <li>It contains the specified table.</li>
+     * <li>Its completion timestamp is before the PITR retention window start 
time.</li>
+     * <li>Its completion timestamp is on or after the table’s continuous 
backup start time.</li>
+     * </ul>
+     * @param backupInfo             The backup information being evaluated.
+     * @param tableName              The table for which PITR validity is 
being checked.
+     * @param continuousBackupTables A map of tables to their continuous 
backup start time.
+     * @param pitrMaxStartTime       The maximum allowed start timestamp for 
PITR eligibility.
+     * @return {@code true} if the backup enables recovery to all valid points 
in time for the
+     *         table; {@code false} otherwise.
+     */
+    private boolean isValidPITRBackup(BackupInfo backupInfo, TableName 
tableName,
+      Map<TableName, Long> continuousBackupTables, long pitrMaxStartTime) {
+      // Only FULL backups are mandatory for PITR
+      if (!BackupType.FULL.equals(backupInfo.getType())) {
+        return false;
+      }
+
+      // The backup must include the table to be relevant for PITR
+      if (!backupInfo.getTableNames().contains(tableName)) {
+        return false;
+      }
+
+      // The backup must have been completed before the PITR retention window 
starts,
+      // otherwise, it won't be helpful in cases where the recovery point is 
between
+      // pitrMaxStartTime and the backup completion time.
+      if (backupInfo.getCompleteTs() > pitrMaxStartTime) {
+        return false;
+      }
+
+      // Retrieve the table's continuous backup start time
+      long continuousBackupStartTime = 
continuousBackupTables.getOrDefault(tableName, 0L);
+
+      // The backup must have been started on or after the table’s continuous 
backup start time,
+      // otherwise, it won't be helpful in few cases because we wouldn't have 
the WAL entries
+      // between the backup start time and the continuous backup start time.
+      if (backupInfo.getStartTs() < continuousBackupStartTime) {
+        return false;
+      }
+
+      return true;
+    }
+
     @Override
     protected void printUsage() {
       System.out.println(DELETE_CMD_USAGE);
       Options options = new Options();
       options.addOption(OPTION_KEEP, true, OPTION_KEEP_DESC);
       options.addOption(OPTION_LIST, true, OPTION_BACKUP_LIST_DESC);
+      options.addOption(OPTION_FORCE_DELETE, false, OPTION_FORCE_DELETE_DESC);
 
       HelpFormatter helpFormatter = new HelpFormatter();
       helpFormatter.setLeftPadding(2);
diff --git 
a/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupDelete.java
 
b/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupDelete.java
index 785859c5280..31eaaff5051 100644
--- 
a/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupDelete.java
+++ 
b/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupDelete.java
@@ -19,6 +19,7 @@ package org.apache.hadoop.hbase.backup;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
@@ -32,7 +33,6 @@ import org.apache.hadoop.hbase.testclassification.LargeTests;
 import org.apache.hadoop.hbase.util.EnvironmentEdge;
 import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
 import org.apache.hadoop.util.ToolRunner;
-import org.junit.Assert;
 import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
@@ -138,7 +138,7 @@ public class TestBackupDelete extends TestBackupBase {
       assertTrue(ret == 0);
     } catch (Exception e) {
       LOG.error("failed", e);
-      Assert.fail(e.getMessage());
+      fail(e.getMessage());
     }
     String output = baos.toString();
     LOG.info(baos.toString());
@@ -154,7 +154,7 @@ public class TestBackupDelete extends TestBackupBase {
       assertTrue(ret == 0);
     } catch (Exception e) {
       LOG.error("failed", e);
-      Assert.fail(e.getMessage());
+      fail(e.getMessage());
     }
     output = baos.toString();
     LOG.info(baos.toString());
diff --git 
a/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupDeleteWithContinuousBackupAndPITR.java
 
b/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupDeleteWithContinuousBackupAndPITR.java
new file mode 100644
index 00000000000..919d3e79f72
--- /dev/null
+++ 
b/hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestBackupDeleteWithContinuousBackupAndPITR.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.backup;
+
+import static 
org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS;
+import static 
org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.ONE_DAY_IN_MILLISECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
+import org.apache.hadoop.util.ToolRunner;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
+
+@Category(LargeTests.class)
+public class TestBackupDeleteWithContinuousBackupAndPITR extends 
TestBackupBase {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    
HBaseClassTestRule.forClass(TestBackupDeleteWithContinuousBackupAndPITR.class);
+
+  private BackupSystemTable backupSystemTable;
+  private String backupId1;
+  private String backupId2;
+  private String backupId3;
+  private String backupId4;
+  private String backupId5;
+
+  /**
+   * Sets up the backup environment before each test.
+   * <p>
+   * This includes:
+   * <ul>
+   * <li>Setting a 30-day PITR (Point-In-Time Recovery) window</li>
+   * <li>Registering table2 as a continuous backup table starting 40 days 
ago</li>
+   * <li>Creating a mix of full and incremental backups at specific time 
offsets (using
+   * EnvironmentEdge injection) to simulate scenarios like: - backups outside 
PITR window - valid
+   * PITR backups - incomplete PITR chains</li>
+   * <li>Resetting the system clock after time manipulation</li>
+   * </ul>
+   * This setup enables tests to evaluate deletion behavior of backups based 
on age, table type, and
+   * PITR chain requirements.
+   */
+  @Before
+  public void setup() throws Exception {
+    conf1.setLong(CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS, 30);
+    backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection());
+
+    long currentTime = System.currentTimeMillis();
+    long backupStartTime = currentTime - 40 * ONE_DAY_IN_MILLISECONDS;
+    backupSystemTable.addContinuousBackupTableSet(Set.of(table2), 
backupStartTime);
+
+    backupId1 = fullTableBackup(Lists.newArrayList(table1));
+    assertTrue(checkSucceeded(backupId1));
+
+    // 31 days back
+    EnvironmentEdgeManager
+      .injectEdge(() -> System.currentTimeMillis() - 31 * 
ONE_DAY_IN_MILLISECONDS);
+    backupId2 = fullTableBackup(Lists.newArrayList(table2));
+    assertTrue(checkSucceeded(backupId2));
+
+    // 32 days back
+    EnvironmentEdgeManager
+      .injectEdge(() -> System.currentTimeMillis() - 32 * 
ONE_DAY_IN_MILLISECONDS);
+    backupId3 = fullTableBackup(Lists.newArrayList(table2));
+    assertTrue(checkSucceeded(backupId3));
+
+    // 15 days back
+    EnvironmentEdgeManager
+      .injectEdge(() -> System.currentTimeMillis() - 15 * 
ONE_DAY_IN_MILLISECONDS);
+    backupId4 = fullTableBackup(Lists.newArrayList(table2));
+    assertTrue(checkSucceeded(backupId4));
+
+    // Reset clock
+    EnvironmentEdgeManager.reset();
+
+    backupId5 = incrementalTableBackup(Lists.newArrayList(table1));
+    assertTrue(checkSucceeded(backupId5));
+  }
+
+  @After
+  public void teardown() throws Exception {
+    EnvironmentEdgeManager.reset();
+    // Try to delete all backups forcefully if they exist
+    for (String id : List.of(backupId1, backupId2, backupId3, backupId4, 
backupId5)) {
+      try {
+        deleteBackup(id, true);
+      } catch (Exception ignored) {
+      }
+    }
+  }
+
+  @Test
+  public void testDeleteIncrementalBackup() throws Exception {
+    assertDeletionSucceeds(backupSystemTable, backupId5, false);
+  }
+
+  @Test
+  public void testDeleteFullBackupNonContinuousTable() throws Exception {
+    assertDeletionSucceeds(backupSystemTable, backupId1, false);
+  }
+
+  @Test
+  public void testDeletePITRIncompleteBackup() throws Exception {
+    assertDeletionSucceeds(backupSystemTable, backupId4, false);
+  }
+
+  @Test
+  public void testDeleteValidPITRBackupWithAnotherPresent() throws Exception {
+    assertDeletionSucceeds(backupSystemTable, backupId2, false);
+  }
+
+  @Test
+  public void testDeleteOnlyValidPITRBackupFails() throws Exception {
+    // Delete backupId2 (31 days ago) — this should succeed
+    assertDeletionSucceeds(backupSystemTable, backupId2, false);
+
+    // Now backupId3 (32 days ago) is the only remaining PITR backup — 
deletion should fail
+    assertDeletionFails(backupSystemTable, backupId3, false);
+  }
+
+  @Test
+  public void testForceDeleteOnlyValidPITRBackup() throws Exception {
+    // Delete backupId2 (31 days ago)
+    assertDeletionSucceeds(backupSystemTable, backupId2, false);
+
+    // Force delete backupId3 — should succeed despite PITR constraints
+    assertDeletionSucceeds(backupSystemTable, backupId3, true);
+  }
+
+  private void assertDeletionSucceeds(BackupSystemTable table, String backupId,
+    boolean isForceDelete) throws Exception {
+    int ret = deleteBackup(backupId, isForceDelete);
+    assertEquals(0, ret);
+    assertFalse("Backup should be deleted but still exists!", 
backupExists(table, backupId));
+  }
+
+  private void assertDeletionFails(BackupSystemTable table, String backupId, 
boolean isForceDelete)
+    throws Exception {
+    int ret = deleteBackup(backupId, isForceDelete);
+    assertNotEquals(0, ret);
+    assertTrue("Backup should still exist after failed deletion!", 
backupExists(table, backupId));
+  }
+
+  private boolean backupExists(BackupSystemTable table, String backupId) 
throws Exception {
+    return table.getBackupHistory().stream()
+      .anyMatch(backup -> backup.getBackupId().equals(backupId));
+  }
+
+  private int deleteBackup(String backupId, boolean isForceDelete) throws 
Exception {
+    String[] args = buildBackupDeleteArgs(backupId, isForceDelete);
+    return ToolRunner.run(conf1, new BackupDriver(), args);
+  }
+
+  private String[] buildBackupDeleteArgs(String backupId, boolean 
isForceDelete) {
+    return isForceDelete
+      ? new String[] { "delete", "-l", backupId, "-fd" }
+      : new String[] { "delete", "-l", backupId };
+  }
+}

Reply via email to