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.filter; 019 020import static org.junit.jupiter.api.Assertions.assertArrayEquals; 021import static org.junit.jupiter.api.Assertions.assertEquals; 022import static org.junit.jupiter.api.Assertions.assertFalse; 023import static org.junit.jupiter.api.Assertions.assertNotNull; 024import static org.mockito.Mockito.mockStatic; 025 026import java.lang.reflect.Field; 027import java.util.Arrays; 028import java.util.Collections; 029import org.apache.hadoop.hbase.Cell; 030import org.apache.hadoop.hbase.KeyValue; 031import org.apache.hadoop.hbase.KeyValueUtil; 032import org.apache.hadoop.hbase.testclassification.FilterTests; 033import org.apache.hadoop.hbase.testclassification.SmallTests; 034import org.apache.hadoop.hbase.unsafe.HBasePlatformDependent; 035import org.apache.hadoop.hbase.util.Bytes; 036import org.apache.hadoop.hbase.util.Pair; 037import org.junit.jupiter.api.BeforeAll; 038import org.junit.jupiter.api.Tag; 039import org.junit.jupiter.api.Test; 040import org.mockito.MockedStatic; 041 042import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations; 043 044import org.apache.hadoop.hbase.shaded.protobuf.generated.FilterProtos; 045import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BytesBytesPair; 046 047/** 048 * Exercises {@link FuzzyRowFilter} on the "no-unsafe" code path 049 * ({@code HBasePlatformDependent.unaligned() == false}) end-to-end, i.e. through the constructor, 050 * {@link FuzzyRowFilter#filterCell} and {@link FuzzyRowFilter#getNextCellHint}. On this path the 051 * mask uses the {0 (fixed), 1 (non-fixed)} encoding, which differs from the unsafe {-1, 2} encoding 052 * that the rest of the scan machinery assumes. 053 */ 054@Tag(FilterTests.TAG) 055@Tag(SmallTests.TAG) 056public class TestFuzzyRowFilterWoUnsafe { 057 058 @BeforeAll 059 public static void disableUnsafe() throws Exception { 060 // Force the no-unsafe path: make HBasePlatformDependent.unaligned() return false and trigger 061 // FuzzyRowFilter's static initializer while the mock is active, so its private static final 062 // UNSAFE_UNALIGNED is captured as false for this JVM fork. 063 try (MockedStatic<HBasePlatformDependent> mocked = mockStatic(HBasePlatformDependent.class)) { 064 mocked.when(HBasePlatformDependent::isUnsafeAvailable).thenReturn(false); 065 mocked.when(HBasePlatformDependent::unaligned).thenReturn(false); 066 Field field = FuzzyRowFilter.class.getDeclaredField("UNSAFE_UNALIGNED"); 067 field.setAccessible(true); 068 assertFalse(field.getBoolean(null), "expected FuzzyRowFilter to use the no-unsafe path"); 069 } 070 } 071 072 /** 073 * A row that matches the fuzzy rule only thanks to a wildcard position must be INCLUDEd. On the 074 * broken no-unsafe path the mask was shifted to all-zeroes (all positions treated as fixed), so 075 * the wildcard row was wrongly rejected. 076 */ 077 @Test 078 public void testForwardMatchesWildcardRow() { 079 FuzzyRowFilter filter = new FuzzyRowFilter( 080 Collections.singletonList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 }))); 081 082 KeyValue match = KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 }); 083 assertEquals(Filter.ReturnCode.INCLUDE, filter.filterCell(match)); 084 } 085 086 /** 087 * For a non-matching row the next-cell hint must be the smallest row that can satisfy the rule. 088 * With fixed positions 5 and 5 and a wildcard in the middle, the smallest row at or after {3,0,0} 089 * is {5,0,5}. On the broken no-unsafe path the wildcard key byte was never cleared and the mask 090 * was corrupted, producing a wrong hint. 091 */ 092 @Test 093 public void testForwardHintSkipsToSmallestMatchingRow() { 094 FuzzyRowFilter filter = new FuzzyRowFilter( 095 Collections.singletonList(new Pair<>(new byte[] { 5, 100, 5 }, new byte[] { 0, 1, 0 }))); 096 097 KeyValue current = KeyValueUtil.createFirstOnRow(new byte[] { 3, 0, 0 }); 098 assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, filter.filterCell(current)); 099 100 Cell hint = filter.getNextCellHint(current); 101 assertRow(new byte[] { 5, 0, 5 }, hint); 102 } 103 104 /** 105 * No-unsafe analogue of {@code TestFuzzyRowFilter#testReverseFilterCellSkipsSameRowHint}. This is 106 * the most subtle composition point this change touches: the reverse next-cell hint is computed 107 * from the no-unsafe mask (updateWith -> preprocessMaskForHinting({0,1}->{-1,0}) -> 108 * getNextForFuzzyRule(reverse)) and must still play with the HBASE-30226 same-row short-circuit. 109 * A non-matching row seeks back to "abb"; revisiting "abb" must be skipped with NEXT_ROW instead 110 * of recreating the same hint. 111 */ 112 @Test 113 public void testReverseHintSkipsSameRow() { 114 FuzzyRowFilter filter = new FuzzyRowFilter( 115 Collections.singletonList(new Pair<>(Bytes.toBytes("aaa"), new byte[] { 0, 1, 0 }))); 116 filter.setReversed(true); 117 118 KeyValue abc = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abc")); 119 assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, filter.filterCell(abc)); 120 assertRow(Bytes.toBytes("abb"), filter.getNextCellHint(abc)); 121 122 KeyValue abb = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abb")); 123 assertEquals(Filter.ReturnCode.NEXT_ROW, filter.filterCell(abb)); 124 } 125 126 /** 127 * With multiple fuzzy keys the RowTracker priority queue holds one (separately converted) hint 128 * mask per key and must return the smallest matching row across all of them. Here key1 {5,*,5} 129 * hints {5,0,5} and key2 {4,9,9} hints {4,9,9}; the smaller {4,9,9} must win. 130 */ 131 @Test 132 public void testForwardHintWithMultipleKeysReturnsSmallest() { 133 FuzzyRowFilter filter = 134 new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 5, 100, 5 }, new byte[] { 0, 1, 0 }), 135 new Pair<>(new byte[] { 4, 9, 9 }, new byte[] { 0, 0, 0 }))); 136 137 KeyValue current = KeyValueUtil.createFirstOnRow(new byte[] { 3, 0, 0 }); 138 assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, filter.filterCell(current)); 139 assertRow(new byte[] { 4, 9, 9 }, filter.getNextCellHint(current)); 140 } 141 142 /** 143 * A FuzzyRowFilter serialized by an unsafe peer puts the mask on the wire in its preprocessed {-1 144 * (fixed), 2 (non-fixed)} form. A no-unsafe server must still interpret it correctly: this is 145 * exactly the {key, mask} pair {@link FuzzyRowFilter#parseFrom} hands to the constructor for such 146 * a filter. Before the fix the no-unsafe constructor left {-1, 2} untouched, so 147 * {@link FuzzyRowFilter#satisfiesNoUnsafe} (which keys off {0, 1}) treated every position as a 148 * wildcard and stopped enforcing the fixed bytes, wrongly INCLUDEing non-matching rows. 149 */ 150 @Test 151 public void testUnsafeEncodedMaskFromPeerEnforcesFixedPositions() { 152 // {-1, 2, -1} == fixed, non-fixed, fixed -> equivalent no-unsafe mask {0, 1, 0}. 153 FuzzyRowFilter match = new FuzzyRowFilter( 154 Collections.singletonList(new Pair<>(new byte[] { 1, 0, 3 }, new byte[] { -1, 2, -1 }))); 155 assertEquals(Filter.ReturnCode.INCLUDE, 156 match.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 }))); 157 158 // A row that differs at a FIXED position (pos 2: 9 != 3) must not be INCLUDEd. 159 FuzzyRowFilter reject = new FuzzyRowFilter( 160 Collections.singletonList(new Pair<>(new byte[] { 1, 0, 3 }, new byte[] { -1, 2, -1 }))); 161 assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, 162 reject.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 9 }))); 163 } 164 165 /** 166 * Faithful wire-level companion to {@link #testUnsafeEncodedMaskFromPeerEnforcesFixedPositions}: 167 * build the exact protobuf bytes an unsafe peer emits (mask in the preprocessed {-1, 2} form) and 168 * run them through {@link FuzzyRowFilter#parseFrom}. A no-unsafe server must normalize the mask 169 * back to {0, 1} on deserialization and still enforce the fixed positions. 170 */ 171 @Test 172 public void testParseFromUnsafeEncodedFilterEnforcesFixedPositions() throws Exception { 173 byte[] wire = FilterProtos.FuzzyRowFilter.newBuilder() 174 .addFuzzyKeysData(BytesBytesPair.newBuilder() 175 .setFirst(UnsafeByteOperations.unsafeWrap(new byte[] { 1, 0, 3 })) 176 .setSecond(UnsafeByteOperations.unsafeWrap(new byte[] { -1, 2, -1 }))) 177 .build().toByteArray(); 178 179 assertEquals(Filter.ReturnCode.INCLUDE, FuzzyRowFilter.parseFrom(wire) 180 .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 }))); 181 // A row that differs at a FIXED position (pos 2: 9 != 3) must not be INCLUDEd. 182 assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, FuzzyRowFilter.parseFrom(wire) 183 .filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 9 }))); 184 } 185 186 /** 187 * A filter built on a no-unsafe server must survive its own {@link FuzzyRowFilter#toByteArray} / 188 * {@link FuzzyRowFilter#parseFrom} round-trip with identical behavior and identical 189 * {@code equals}/{@code hashCode} (the wire form is the canonical {0, 1}). 190 */ 191 @Test 192 public void testSerializationRoundTripPreservesFilter() throws Exception { 193 FuzzyRowFilter original = new FuzzyRowFilter( 194 Collections.singletonList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 }))); 195 FuzzyRowFilter parsed = FuzzyRowFilter.parseFrom(original.toByteArray()); 196 197 assertEquals(original, parsed); 198 assertEquals(original.hashCode(), parsed.hashCode()); 199 assertEquals(Filter.ReturnCode.INCLUDE, 200 parsed.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 }))); 201 } 202 203 private static void assertRow(byte[] expected, Cell cell) { 204 byte[] actual = Bytes.copy(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength()); 205 assertEquals(Bytes.toStringBinary(expected), Bytes.toStringBinary(actual)); 206 } 207 208 // --------------------------------------------------------------------------- 209 // Fine-grained unit coverage of the mask/key preprocessing helpers used above. 210 // --------------------------------------------------------------------------- 211 212 @Test 213 public void testPreprocessMaskForSatisfiesNoUnsafeKeepsMaskSemantics() { 214 byte[] row = new byte[] { 1, 2, 1, 3, 3 }; 215 byte[] fuzzyKey = new byte[] { 1, 2, 0, 3 }; 216 byte[] fuzzyKeyFiltered = new byte[] { 0, 2, 0, 3 }; 217 byte[] mask = new byte[] { 0, 0, 1, 0 }; 218 219 assertEquals(FuzzyRowFilter.SatisfiesCode.YES, 220 FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length, fuzzyKey, mask)); 221 assertEquals(FuzzyRowFilter.SatisfiesCode.NO_NEXT, 222 FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length, fuzzyKeyFiltered, mask)); 223 224 // On the no-unsafe path the mask must be left untouched (the broken code shifted it to 0s). 225 FuzzyRowFilter.preprocessMaskForSatisfies(mask, false); 226 assertArrayEquals(new byte[] { 0, 0, 1, 0 }, mask); 227 228 assertEquals(FuzzyRowFilter.SatisfiesCode.YES, 229 FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length, fuzzyKey, mask)); 230 assertEquals(FuzzyRowFilter.SatisfiesCode.NO_NEXT, 231 FuzzyRowFilter.satisfiesNoUnsafe(false, row, 0, row.length, fuzzyKeyFiltered, mask)); 232 } 233 234 @Test 235 public void testPreprocessMaskForHintingNoUnsafeConvertsToGetNextSemantics() { 236 byte[] row = new byte[] { 1, 2, 1, 3, 3 }; 237 byte[] fuzzyKey = new byte[] { 1, 2, 0, 3 }; 238 byte[] noUnsafeMask = new byte[] { 0, 0, 1, 0 }; 239 240 byte[] convertedMask = FuzzyRowFilter.preprocessMaskForHinting(noUnsafeMask, false); 241 // The original mask must not be mutated (satisfiesNoUnsafe still needs the {0, 1} form). 242 assertArrayEquals(new byte[] { 0, 0, 1, 0 }, noUnsafeMask); 243 assertArrayEquals(new byte[] { -1, -1, 0, -1 }, convertedMask); 244 245 byte[] nextWithConverted = 246 FuzzyRowFilter.getNextForFuzzyRule(false, row, 0, row.length, fuzzyKey, convertedMask); 247 byte[] nextWithExpectedMask = FuzzyRowFilter.getNextForFuzzyRule(false, row, 0, row.length, 248 fuzzyKey, new byte[] { -1, -1, 0, -1 }); 249 250 assertNotNull(nextWithConverted); 251 assertArrayEquals(nextWithExpectedMask, nextWithConverted); 252 } 253 254 @Test 255 public void testNoUnsafePreprocessSearchKeyClearsWildcardBytes() { 256 Pair<byte[], byte[]> fuzzyData = new Pair<>(new byte[] { 1, 100, 3 }, new byte[] { 0, 1, 0 }); 257 258 FuzzyRowFilter.preprocessSearchKey(fuzzyData, false); 259 byte[] convertedMask = FuzzyRowFilter.preprocessMaskForHinting(fuzzyData.getSecond(), false); 260 byte[] nextForFuzzyRule = FuzzyRowFilter.getNextForFuzzyRule(false, new byte[] { 0, 0, 0 }, 0, 261 3, fuzzyData.getFirst(), convertedMask); 262 263 // The wildcard byte (100) must be cleared so the next hint is the smallest matching row. 264 assertArrayEquals(new byte[] { 1, 0, 3 }, fuzzyData.getFirst()); 265 assertArrayEquals(new byte[] { 1, 0, 3 }, nextForFuzzyRule); 266 } 267}