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 -&gt; preprocessMaskForHinting({0,1}-&gt;{-1,0}) -&gt;
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}