Skip to content

Commit 78effd5

Browse files
committed
HBASE-30036 Fix redundant DeleteColumn with empty qualifier skipping DeleteFamily
1 parent 7f9622e commit 78effd5

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/querymatcher/MinorCompactionScanQueryMatcher.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.io.IOException;
2121
import org.apache.hadoop.hbase.ExtendedCell;
22+
import org.apache.hadoop.hbase.KeyValue;
2223
import org.apache.hadoop.hbase.PrivateCellUtil;
2324
import org.apache.hadoop.hbase.regionserver.ScanInfo;
2425
import org.apache.yetus.audience.InterfaceAudience;
@@ -52,6 +53,12 @@ public MatchCode match(ExtendedCell cell) throws IOException {
5253
// that overwrites tracker state. Seek past remaining cells for this column/row since
5354
// they are all covered by the previously tracked delete.
5455
if (deletes.isRedundantDelete(cell)) {
56+
// Skip seeking for deletes with empty qualifier, not to skip a subsequent
57+
// DeleteFamily marker that covers other qualifiers. DeleteFamily itself can seek
58+
// safely because all remaining empty-qualifier cells are redundant under it.
59+
if (cell.getQualifierLength() == 0 && typeByte != KeyValue.Type.DeleteFamily.getCode()) {
60+
return MatchCode.SKIP;
61+
}
5562
return columns.getNextRowOrNextColumn(cell);
5663
}
5764
trackDelete(cell);

hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/querymatcher/TestCompactionScanQueryMatcher.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,10 @@ public void testSkipsRedundantDeleteMarkers() throws IOException {
9494
assertRetainDeletes(new Type[] { Type.DeleteFamily, Type.DeleteFamily, Type.DeleteFamily },
9595
INCLUDE, SEEK_NEXT_COL, SEEK_NEXT_COL);
9696

97-
// DF + DFV interleaved. DF included, DFV at same ts redundant, older DF redundant.
97+
// DF + DFV interleaved. DF included, DFV redundant (SKIP because empty qualifier),
98+
// older DF redundant (SEEK_NEXT_COL), older DFV redundant (SKIP).
9899
assertRetainDeletes(new Type[] { Type.DeleteFamily, Type.DeleteFamilyVersion, Type.DeleteFamily,
99-
Type.DeleteFamilyVersion }, INCLUDE, SEEK_NEXT_COL, SEEK_NEXT_COL, SEEK_NEXT_COL);
100+
Type.DeleteFamilyVersion }, INCLUDE, SKIP, SEEK_NEXT_COL, SKIP);
100101

101102
// Delete (version) covered by DeleteColumn.
102103
assertRetainDeletes(new Type[] { Type.DeleteColumn, Type.Delete, Type.Delete, Type.Delete },
@@ -108,17 +109,51 @@ public void testSkipsRedundantDeleteMarkers() throws IOException {
108109
INCLUDE);
109110
}
110111

112+
/**
113+
* Redundant column-level deletes with empty qualifier must not seek past a subsequent
114+
* DeleteFamily. getKeyForNextColumn treats empty qualifier as "no column" and returns
115+
* SEEK_NEXT_ROW, which would skip the DF and all remaining cells in the row.
116+
*/
117+
@Test
118+
public void testEmptyQualifierDeleteDoesNotSkipDeleteFamily() throws IOException {
119+
byte[] emptyQualifier = HConstants.EMPTY_BYTE_ARRAY;
120+
121+
// DC(empty) + DC(empty) redundant + DF must still be reachable.
122+
assertRetainDeletes(emptyQualifier,
123+
new Type[] { Type.DeleteColumn, Type.DeleteColumn, Type.DeleteFamily }, INCLUDE, SKIP,
124+
INCLUDE);
125+
126+
// DC(empty) + Delete(empty) redundant + DF must still be reachable.
127+
assertRetainDeletes(emptyQualifier,
128+
new Type[] { Type.DeleteColumn, Type.Delete, Type.DeleteFamily }, INCLUDE, SKIP, INCLUDE);
129+
}
130+
111131
private void assertRetainDeletes(Type[] types, MatchCode... expected) throws IOException {
112132
assertRetainDeletes(KeepDeletedCells.FALSE, types, expected);
113133
}
114134

135+
private void assertRetainDeletes(byte[] qualifier, Type[] types, MatchCode... expected)
136+
throws IOException {
137+
assertRetainDeletes(KeepDeletedCells.FALSE, qualifier, types, expected);
138+
}
139+
115140
/**
116141
* Build cells from the given types with decrementing timestamps (same ts for adjacent
117142
* family-level and column-level types at the same position). Family-level types (DeleteFamily,
118143
* DeleteFamilyVersion) use empty qualifier; others use col1.
119144
*/
120145
private void assertRetainDeletes(KeepDeletedCells keepDeletedCells, Type[] types,
121146
MatchCode... expected) throws IOException {
147+
assertRetainDeletes(keepDeletedCells, null, types, expected);
148+
}
149+
150+
/**
151+
* Build cells from the given types with decrementing timestamps. If qualifier is null,
152+
* family-level types use empty qualifier and others use col1. If qualifier is specified, all
153+
* types use that qualifier.
154+
*/
155+
private void assertRetainDeletes(KeepDeletedCells keepDeletedCells, byte[] qualifier,
156+
Type[] types, MatchCode... expected) throws IOException {
122157
long now = EnvironmentEdgeManager.currentTime();
123158
ScanInfo scanInfo = new ScanInfo(this.conf, fam1, 0, 1, ttl, keepDeletedCells,
124159
HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false);
@@ -130,8 +165,13 @@ private void assertRetainDeletes(KeepDeletedCells keepDeletedCells, Type[] types
130165
long ts = now;
131166
List<MatchCode> actual = new ArrayList<>(expected.length);
132167
for (int i = 0; i < types.length; i++) {
133-
boolean familyLevel = types[i] == Type.DeleteFamily || types[i] == Type.DeleteFamilyVersion;
134-
byte[] qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1;
168+
byte[] qual;
169+
if (qualifier != null) {
170+
qual = qualifier;
171+
} else {
172+
boolean familyLevel = types[i] == Type.DeleteFamily || types[i] == Type.DeleteFamilyVersion;
173+
qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1;
174+
}
135175
KeyValue kv = types[i] == Type.Put
136176
? new KeyValue(row1, fam1, qual, ts, types[i], data)
137177
: new KeyValue(row1, fam1, qual, ts, types[i]);

0 commit comments

Comments
 (0)