Skip to content

Commit e2bece6

Browse files
rmdmattinglyhgromerHernan Gelaf-Romer
authored
HBASE-28897 Force CF compatibility during incremental backup (#6340) (#6358)
Signed-off-by: Ray Mattingly <[email protected]> Co-authored-by: Hernan Romer <[email protected]> Co-authored-by: Hernan Gelaf-Romer <[email protected]>
1 parent 30c9cb0 commit e2bece6

File tree

3 files changed

+190
-8
lines changed

3 files changed

+190
-8
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.backup.impl;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import org.apache.commons.lang3.StringUtils;
23+
import org.apache.hadoop.hbase.TableName;
24+
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
25+
import org.apache.yetus.audience.InterfaceAudience;
26+
27+
@InterfaceAudience.Public
28+
public final class ColumnFamilyMismatchException extends BackupException {
29+
private final List<TableName> mismatchedTables;
30+
31+
private ColumnFamilyMismatchException(String msg, List<TableName> mismatchedTables) {
32+
super(msg);
33+
this.mismatchedTables = mismatchedTables;
34+
}
35+
36+
public static final class ColumnFamilyMismatchExceptionBuilder {
37+
private final List<TableName> mismatchedTables = new ArrayList<>();
38+
private final StringBuilder msg = new StringBuilder();
39+
40+
public ColumnFamilyMismatchExceptionBuilder addMismatchedTable(TableName tableName,
41+
ColumnFamilyDescriptor[] currentCfs, ColumnFamilyDescriptor[] backupCfs) {
42+
this.mismatchedTables.add(tableName);
43+
44+
String currentCfsParsed = StringUtils.join(currentCfs, ',');
45+
String backupCfsParsed = StringUtils.join(backupCfs, ',');
46+
msg.append("\nMismatch in column family descriptor for table: ").append(tableName)
47+
.append("\n");
48+
msg.append("Current families: ").append(currentCfsParsed).append("\n");
49+
msg.append("Backup families: ").append(backupCfsParsed);
50+
51+
return this;
52+
}
53+
54+
public ColumnFamilyMismatchException build() {
55+
return new ColumnFamilyMismatchException(msg.toString(), mismatchedTables);
56+
}
57+
}
58+
59+
public List<TableName> getMismatchedTables() {
60+
return mismatchedTables;
61+
}
62+
63+
public static ColumnFamilyMismatchExceptionBuilder newBuilder() {
64+
return new ColumnFamilyMismatchExceptionBuilder();
65+
}
66+
}

hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/IncrementalTableBackupClient.java

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.net.URI;
2424
import java.net.URISyntaxException;
2525
import java.util.ArrayList;
26+
import java.util.HashMap;
2627
import java.util.List;
2728
import java.util.Map;
2829
import java.util.Set;
@@ -32,16 +33,21 @@
3233
import org.apache.hadoop.fs.Path;
3334
import org.apache.hadoop.hbase.TableName;
3435
import org.apache.hadoop.hbase.backup.BackupCopyJob;
36+
import org.apache.hadoop.hbase.backup.BackupInfo;
3537
import org.apache.hadoop.hbase.backup.BackupInfo.BackupPhase;
3638
import org.apache.hadoop.hbase.backup.BackupRequest;
3739
import org.apache.hadoop.hbase.backup.BackupRestoreFactory;
3840
import org.apache.hadoop.hbase.backup.BackupType;
41+
import org.apache.hadoop.hbase.backup.HBackupFileSystem;
3942
import org.apache.hadoop.hbase.backup.mapreduce.MapReduceBackupCopyJob;
4043
import org.apache.hadoop.hbase.backup.util.BackupUtils;
4144
import org.apache.hadoop.hbase.client.Admin;
45+
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
4246
import org.apache.hadoop.hbase.client.Connection;
4347
import org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2;
4448
import org.apache.hadoop.hbase.mapreduce.WALPlayer;
49+
import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils;
50+
import org.apache.hadoop.hbase.snapshot.SnapshotManifest;
4551
import org.apache.hadoop.hbase.util.Bytes;
4652
import org.apache.hadoop.hbase.util.CommonFSUtils;
4753
import org.apache.hadoop.hbase.util.HFileArchiveUtil;
@@ -52,6 +58,8 @@
5258
import org.slf4j.Logger;
5359
import org.slf4j.LoggerFactory;
5460

61+
import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos;
62+
5563
/**
5664
* Incremental backup implementation. See the {@link #execute() execute} method.
5765
*/
@@ -261,8 +269,11 @@ private void updateFileLists(List<String> activeFiles, List<String> archiveFiles
261269
}
262270

263271
@Override
264-
public void execute() throws IOException {
272+
public void execute() throws IOException, ColumnFamilyMismatchException {
265273
try {
274+
Map<TableName, String> tablesToFullBackupIds = getFullBackupIds();
275+
verifyCfCompatibility(backupInfo.getTables(), tablesToFullBackupIds);
276+
266277
// case PREPARE_INCREMENTAL:
267278
beginBackup(backupManager, backupInfo);
268279
backupInfo.setPhase(BackupPhase.PREPARE_INCREMENTAL);
@@ -436,4 +447,86 @@ protected Path getBulkOutputDir() {
436447
path = new Path(path, backupId);
437448
return path;
438449
}
450+
451+
private Map<TableName, String> getFullBackupIds() throws IOException {
452+
// Ancestors are stored from newest to oldest, so we can iterate backwards
453+
// in order to populate our backupId map with the most recent full backup
454+
// for a given table
455+
List<BackupManifest.BackupImage> images = getAncestors(backupInfo);
456+
Map<TableName, String> results = new HashMap<>();
457+
for (int i = images.size() - 1; i >= 0; i--) {
458+
BackupManifest.BackupImage image = images.get(i);
459+
if (image.getType() != BackupType.FULL) {
460+
continue;
461+
}
462+
463+
for (TableName tn : image.getTableNames()) {
464+
results.put(tn, image.getBackupId());
465+
}
466+
}
467+
return results;
468+
}
469+
470+
/**
471+
* Verifies that the current table descriptor CFs matches the descriptor CFs of the last full
472+
* backup for the tables. This ensures CF compatibility across incremental backups. If a mismatch
473+
* is detected, a full table backup should be taken, rather than an incremental one
474+
*/
475+
private void verifyCfCompatibility(Set<TableName> tables,
476+
Map<TableName, String> tablesToFullBackupId) throws IOException, ColumnFamilyMismatchException {
477+
ColumnFamilyMismatchException.ColumnFamilyMismatchExceptionBuilder exBuilder =
478+
ColumnFamilyMismatchException.newBuilder();
479+
try (Admin admin = conn.getAdmin(); BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
480+
for (TableName tn : tables) {
481+
String backupId = tablesToFullBackupId.get(tn);
482+
BackupInfo fullBackupInfo = backupAdmin.getBackupInfo(backupId);
483+
484+
ColumnFamilyDescriptor[] currentCfs = admin.getDescriptor(tn).getColumnFamilies();
485+
String snapshotName = fullBackupInfo.getSnapshotName(tn);
486+
Path root = HBackupFileSystem.getTableBackupPath(tn,
487+
new Path(fullBackupInfo.getBackupRootDir()), fullBackupInfo.getBackupId());
488+
Path manifestDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, root);
489+
490+
FileSystem fs;
491+
try {
492+
fs = FileSystem.get(new URI(fullBackupInfo.getBackupRootDir()), conf);
493+
} catch (URISyntaxException e) {
494+
throw new IOException("Unable to get fs", e);
495+
}
496+
497+
SnapshotProtos.SnapshotDescription snapshotDescription =
498+
SnapshotDescriptionUtils.readSnapshotInfo(fs, manifestDir);
499+
SnapshotManifest manifest =
500+
SnapshotManifest.open(conf, fs, manifestDir, snapshotDescription);
501+
502+
ColumnFamilyDescriptor[] backupCfs = manifest.getTableDescriptor().getColumnFamilies();
503+
if (!areCfsCompatible(currentCfs, backupCfs)) {
504+
exBuilder.addMismatchedTable(tn, currentCfs, backupCfs);
505+
}
506+
}
507+
}
508+
509+
ColumnFamilyMismatchException ex = exBuilder.build();
510+
if (!ex.getMismatchedTables().isEmpty()) {
511+
throw ex;
512+
}
513+
}
514+
515+
private static boolean areCfsCompatible(ColumnFamilyDescriptor[] currentCfs,
516+
ColumnFamilyDescriptor[] backupCfs) {
517+
if (currentCfs.length != backupCfs.length) {
518+
return false;
519+
}
520+
521+
for (int i = 0; i < backupCfs.length; i++) {
522+
String currentCf = currentCfs[i].getNameAsString();
523+
String backupCf = backupCfs[i].getNameAsString();
524+
525+
if (!currentCf.equals(backupCf)) {
526+
return false;
527+
}
528+
}
529+
530+
return true;
531+
}
439532
}

hbase-backup/src/test/java/org/apache/hadoop/hbase/backup/TestIncrementalBackup.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
package org.apache.hadoop.hbase.backup;
1919

2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertThrows;
2122
import static org.junit.Assert.assertTrue;
2223

24+
import java.io.IOException;
2325
import java.util.ArrayList;
2426
import java.util.Collection;
2527
import java.util.HashSet;
@@ -30,6 +32,7 @@
3032
import org.apache.hadoop.hbase.TableName;
3133
import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl;
3234
import org.apache.hadoop.hbase.backup.impl.BackupManifest;
35+
import org.apache.hadoop.hbase.backup.impl.ColumnFamilyMismatchException;
3336
import org.apache.hadoop.hbase.backup.util.BackupUtils;
3437
import org.apache.hadoop.hbase.client.Admin;
3538
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
@@ -52,6 +55,8 @@
5255
import org.slf4j.Logger;
5356
import org.slf4j.LoggerFactory;
5457

58+
import org.apache.hbase.thirdparty.com.google.common.base.Throwables;
59+
import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableList;
5560
import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
5661
import org.apache.hbase.thirdparty.com.google.common.collect.Sets;
5762

@@ -101,9 +106,7 @@ public void TestIncBackupRestore() throws Exception {
101106
insertIntoTable(conn, table1, mobName, 3, NB_ROWS_FAM3).close();
102107
Admin admin = conn.getAdmin();
103108
BackupAdminImpl client = new BackupAdminImpl(conn);
104-
BackupRequest request = createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR);
105-
String backupIdFull = client.backupTables(request);
106-
assertTrue(checkSucceeded(backupIdFull));
109+
String backupIdFull = takeFullBackup(tables, client);
107110

108111
// #2 - insert some data to table
109112
Table t1 = insertIntoTable(conn, table1, famName, 1, ADD_ROWS);
@@ -138,16 +141,14 @@ public void TestIncBackupRestore() throws Exception {
138141
// exception will be thrown.
139142
LOG.debug("region is not splittable, because " + e);
140143
}
141-
while (!admin.isTableAvailable(table1)) {
142-
Thread.sleep(100);
143-
}
144+
TEST_UTIL.waitTableAvailable(table1);
144145
long endSplitTime = EnvironmentEdgeManager.currentTime();
145146
// split finished
146147
LOG.debug("split finished in =" + (endSplitTime - startSplitTime));
147148

148149
// #3 - incremental backup for multiple tables
149150
tables = Lists.newArrayList(table1, table2);
150-
request = createBackupRequest(BackupType.INCREMENTAL, tables, BACKUP_ROOT_DIR);
151+
BackupRequest request = createBackupRequest(BackupType.INCREMENTAL, tables, BACKUP_ROOT_DIR);
151152
String backupIdIncMultiple = client.backupTables(request);
152153
assertTrue(checkSucceeded(backupIdIncMultiple));
153154
BackupManifest manifest =
@@ -162,6 +163,13 @@ public void TestIncBackupRestore() throws Exception {
162163
.build();
163164
TEST_UTIL.getAdmin().modifyTable(newTable1Desc);
164165

166+
// check that an incremental backup fails because the CFs don't match
167+
final List<TableName> tablesCopy = tables;
168+
IOException ex = assertThrows(IOException.class, () -> client
169+
.backupTables(createBackupRequest(BackupType.INCREMENTAL, tablesCopy, BACKUP_ROOT_DIR)));
170+
checkThrowsCFMismatch(ex, ImmutableList.of(table1));
171+
takeFullBackup(tables, client);
172+
165173
int NB_ROWS_FAM2 = 7;
166174
Table t3 = insertIntoTable(conn, table1, fam2Name, 2, NB_ROWS_FAM2);
167175
t3.close();
@@ -224,4 +232,19 @@ public void TestIncBackupRestore() throws Exception {
224232
admin.close();
225233
}
226234
}
235+
236+
private void checkThrowsCFMismatch(IOException ex, List<TableName> tables) {
237+
Throwable cause = Throwables.getRootCause(ex);
238+
assertEquals(cause.getClass(), ColumnFamilyMismatchException.class);
239+
ColumnFamilyMismatchException e = (ColumnFamilyMismatchException) cause;
240+
assertEquals(tables, e.getMismatchedTables());
241+
}
242+
243+
private String takeFullBackup(List<TableName> tables, BackupAdminImpl backupAdmin)
244+
throws IOException {
245+
BackupRequest req = createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR);
246+
String backupId = backupAdmin.backupTables(req);
247+
checkSucceeded(backupId);
248+
return backupId;
249+
}
227250
}

0 commit comments

Comments
 (0)