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.apache.hadoop.hbase.backup.BackupInfo.withState;
021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS;
022import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.ONE_DAY_IN_MILLISECONDS;
023import static org.junit.jupiter.api.Assertions.assertEquals;
024import static org.junit.jupiter.api.Assertions.assertFalse;
025import static org.junit.jupiter.api.Assertions.assertNotEquals;
026import static org.junit.jupiter.api.Assertions.assertTrue;
027
028import java.io.IOException;
029import java.util.Set;
030import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
031import org.apache.hadoop.hbase.testclassification.LargeTests;
032import org.apache.hadoop.util.ToolRunner;
033import org.junit.jupiter.api.Tag;
034import org.junit.jupiter.api.Test;
035
036import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
037
038/**
039 * Tests the deletion of HBase backups under continuous backup and PITR settings.
040 * <p>
041 * Terminology:
042 * <ul>
043 * <li><b>ct (current time)</b>: Current timestamp</li>
044 * <li><b>maxAllowedPITRTime (mapt)</b>: Maximum allowed time range for PITR, typically a
045 * cluster-level config (e.g., 30 days ago)</li>
046 * <li><b>cst (continuousBackupStartTime)</b>: Earliest time from which continuous backup is
047 * available</li>
048 * <li><b>fs</b>: Full backup start time (not reliably usable)</li>
049 * <li><b>fm</b>: Time when snapshot (logical freeze) was taken (we don't have this)</li>
050 * <li><b>fe</b>: Full backup end time (used as conservative proxy for fm)</li>
051 * </ul>
052 */
053@Tag(LargeTests.TAG)
054public class TestBackupDeleteWithContinuousBackupAndPITR extends TestBackupBase {
055
056  private BackupSystemTable backupSystemTable;
057
058  /**
059   * Configures continuous backup with the specified CST (continuous backup start time).
060   */
061  private void configureContinuousBackup(long cstTimestamp) throws IOException {
062    conf1.setLong(CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS, 30);
063    backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection());
064
065    backupSystemTable.addContinuousBackupTableSet(Set.of(table1), cstTimestamp);
066  }
067
068  private void cleanupContinuousBackup() throws IOException {
069    backupSystemTable.removeContinuousBackupTableSet(Set.of(table1));
070  }
071
072  /**
073   * Main Case: continuousBackupStartTime < maxAllowedPITRTime
074   * <p>
075   * Sub Case: fe < cst
076   */
077  @Test
078  public void testDeletionWhenBackupCompletesBeforeCST() throws Exception {
079    long now = System.currentTimeMillis();
080    long cst = now - 40 * ONE_DAY_IN_MILLISECONDS; // CST = 40 days ago
081    configureContinuousBackup(cst);
082
083    String backupId =
084      createAndUpdateBackup(cst - ONE_DAY_IN_MILLISECONDS, cst - ONE_DAY_IN_MILLISECONDS + 1000);
085    assertDeletionSucceeds(backupSystemTable, backupId, false);
086
087    cleanupContinuousBackup();
088  }
089
090  /**
091   * Main Case: continuousBackupStartTime < maxAllowedPITRTime
092   * <p>
093   * Sub Case: fs < cst < fe
094   */
095  @Test
096  public void testDeletionWhenBackupStraddlesCST() throws Exception {
097    long now = System.currentTimeMillis();
098    long cst = now - 40 * ONE_DAY_IN_MILLISECONDS; // CST = 40 days ago
099    configureContinuousBackup(cst);
100
101    String backupId = createAndUpdateBackup(cst - 1000, cst + 1000);
102    assertDeletionSucceeds(backupSystemTable, backupId, false);
103
104    cleanupContinuousBackup();
105  }
106
107  /**
108   * Main Case: continuousBackupStartTime < maxAllowedPITRTime
109   * <p>
110   * Sub Case: fs >= cst && fe < mapt
111   */
112  @Test
113  public void testDeletionWhenBackupWithinCSTToMAPTRangeAndUncovered() throws Exception {
114    long now = System.currentTimeMillis();
115    long cst = now - 40 * ONE_DAY_IN_MILLISECONDS;
116    long mapt = now - 30 * ONE_DAY_IN_MILLISECONDS;
117    configureContinuousBackup(cst);
118
119    String backupId = createAndUpdateBackup(cst, mapt - 1000);
120    assertDeletionFails(backupSystemTable, backupId);
121
122    // Cover the backup with another backup
123    String coverId = createAndUpdateBackup(cst, mapt - 1000);
124
125    // Now, deletion should succeed because the backup is covered by the new one
126    assertDeletionSucceeds(backupSystemTable, backupId, false);
127    assertDeletionSucceeds(backupSystemTable, coverId, true);
128
129    cleanupContinuousBackup();
130  }
131
132  /**
133   * Main Case: continuousBackupStartTime < maxAllowedPITRTime
134   * <p>
135   * Sub Case: fs >= cst && fe >= mapt
136   */
137  @Test
138  public void testDeletionWhenBackupExtendsBeyondMAPTAndUncovered() throws Exception {
139    long now = System.currentTimeMillis();
140    long cst = now - 40 * ONE_DAY_IN_MILLISECONDS;
141    long mapt = now - 30 * ONE_DAY_IN_MILLISECONDS;
142    configureContinuousBackup(cst);
143
144    String backupId = createAndUpdateBackup(cst + 1000, mapt + 1000);
145    assertDeletionFails(backupSystemTable, backupId);
146
147    // Cover the backup with another backup
148    String coverId = createAndUpdateBackup(cst + 1000, mapt + 1000);
149
150    // Now, deletion should succeed because the backup is covered by the new one
151    assertDeletionSucceeds(backupSystemTable, backupId, false);
152    assertDeletionSucceeds(backupSystemTable, coverId, true);
153
154    cleanupContinuousBackup();
155  }
156
157  /**
158   * Main Case: continuousBackupStartTime >= maxAllowedPITRTime
159   * <p>
160   * Sub Case: fs < cst
161   */
162  @Test
163  public void testDeletionWhenBackupBeforeCST_ShouldSucceed() throws Exception {
164    long now = System.currentTimeMillis();
165    long cst = now - 20 * ONE_DAY_IN_MILLISECONDS;
166    configureContinuousBackup(cst);
167
168    String backupId = createAndUpdateBackup(cst - 1000, cst + 1000);
169    assertDeletionSucceeds(backupSystemTable, backupId, false);
170
171    cleanupContinuousBackup();
172  }
173
174  /**
175   * Main Case: continuousBackupStartTime >= maxAllowedPITRTime
176   * <p>
177   * Sub Case: fs >= cst
178   */
179  @Test
180  public void testDeletionWhenBackupAfterCST_ShouldFailUnlessCovered() throws Exception {
181    long now = System.currentTimeMillis();
182    long cst = now - 20 * ONE_DAY_IN_MILLISECONDS;
183    configureContinuousBackup(cst);
184
185    String backupId = createAndUpdateBackup(cst + 1000, cst + 2000);
186    assertDeletionFails(backupSystemTable, backupId);
187
188    // Cover the backup with another backup
189    String coverId = createAndUpdateBackup(cst + 1000, cst + 2000);
190
191    assertDeletionSucceeds(backupSystemTable, backupId, false);
192    assertDeletionSucceeds(backupSystemTable, coverId, true);
193
194    cleanupContinuousBackup();
195  }
196
197  @Test
198  public void testDeleteIncrementalBackup() throws Exception {
199    long now = System.currentTimeMillis();
200    long cst = now - 20 * ONE_DAY_IN_MILLISECONDS;
201    configureContinuousBackup(cst);
202
203    String fullBackupId = fullTableBackup(Lists.newArrayList(table1));
204    String incrementalTableBackupId = incrementalTableBackup(Lists.newArrayList(table1));
205    assertDeletionSucceeds(backupSystemTable, incrementalTableBackupId, false);
206
207    assertDeletionSucceeds(backupSystemTable, fullBackupId, true);
208  }
209
210  @Test
211  public void testDeleteFullBackupNonContinuousTable() throws Exception {
212    conf1.setLong(CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS, 30);
213    backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection());
214
215    long now = System.currentTimeMillis();
216    String backupId =
217      createAndUpdateBackup(now - ONE_DAY_IN_MILLISECONDS, now - ONE_DAY_IN_MILLISECONDS + 1000);
218    assertDeletionSucceeds(backupSystemTable, backupId, false);
219  }
220
221  /**
222   * Creates a full backup and updates its timestamps.
223   */
224  private String createAndUpdateBackup(long startTs, long completeTs) throws Exception {
225    String backupId = fullTableBackup(Lists.newArrayList(table1));
226    assertTrue(checkSucceeded(backupId));
227
228    BackupInfo backupInfo = getBackupInfoById(backupId);
229    backupInfo.setStartTs(startTs);
230    backupInfo.setCompleteTs(completeTs);
231    backupSystemTable.updateBackupInfo(backupInfo);
232
233    return backupId;
234  }
235
236  private void assertDeletionSucceeds(BackupSystemTable table, String backupId,
237    boolean isForceDelete) throws Exception {
238    int ret = deleteBackup(backupId, isForceDelete);
239    assertEquals(0, ret);
240    assertFalse(backupExists(table, backupId), "Backup should be deleted but still exists!");
241  }
242
243  private void assertDeletionFails(BackupSystemTable table, String backupId) throws Exception {
244    int ret = deleteBackup(backupId, false);
245    assertNotEquals(0, ret);
246    assertTrue(backupExists(table, backupId), "Backup should still exist after failed deletion!");
247  }
248
249  private boolean backupExists(BackupSystemTable table, String backupId) throws Exception {
250    return table.getBackupHistory().stream()
251      .anyMatch(backup -> backup.getBackupId().equals(backupId));
252  }
253
254  private int deleteBackup(String backupId, boolean isForceDelete) throws Exception {
255    String[] args = buildBackupDeleteArgs(backupId, isForceDelete);
256    return ToolRunner.run(conf1, new BackupDriver(), args);
257  }
258
259  private String[] buildBackupDeleteArgs(String backupId, boolean isForceDelete) {
260    return isForceDelete
261      ? new String[] { "delete", "-l", backupId, "-fd" }
262      : new String[] { "delete", "-l", backupId };
263  }
264
265  private BackupInfo getBackupInfoById(String backupId) throws IOException {
266    return backupSystemTable.getBackupHistory(withState(BackupInfo.BackupState.COMPLETE)).stream()
267      .filter(b -> b.getBackupId().equals(backupId)).findFirst()
268      .orElseThrow(() -> new IllegalStateException("Backup should exist: " + backupId));
269  }
270}