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.regionserver.querymatcher; 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.assertNull; 024 025import java.io.IOException; 026import java.util.ArrayList; 027import java.util.List; 028import org.apache.hadoop.hbase.Cell; 029import org.apache.hadoop.hbase.ExtendedCell; 030import org.apache.hadoop.hbase.HConstants; 031import org.apache.hadoop.hbase.KeepDeletedCells; 032import org.apache.hadoop.hbase.KeyValue; 033import org.apache.hadoop.hbase.KeyValue.Type; 034import org.apache.hadoop.hbase.PrivateCellUtil; 035import org.apache.hadoop.hbase.client.Scan; 036import org.apache.hadoop.hbase.filter.FilterBase; 037import org.apache.hadoop.hbase.regionserver.ScanInfo; 038import org.apache.hadoop.hbase.regionserver.querymatcher.ScanQueryMatcher.MatchCode; 039import org.apache.hadoop.hbase.testclassification.RegionServerTests; 040import org.apache.hadoop.hbase.testclassification.SmallTests; 041import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 042import org.junit.jupiter.api.Tag; 043import org.junit.jupiter.api.Test; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047@Tag(RegionServerTests.TAG) 048@Tag(SmallTests.TAG) 049public class TestUserScanQueryMatcher extends AbstractTestScanQueryMatcher { 050 051 private static final Logger LOG = LoggerFactory.getLogger(TestUserScanQueryMatcher.class); 052 053 /** 054 * This is a cryptic test. It is checking that we don't include a fake cell. See HBASE-16074 for 055 * background. 056 */ 057 @Test 058 public void testNeverIncludeFakeCell() throws IOException { 059 long now = EnvironmentEdgeManager.currentTime(); 060 // Do with fam2 which has a col2 qualifier. 061 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, 062 new ScanInfo(this.conf, fam2, 10, 1, ttl, KeepDeletedCells.FALSE, 063 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 064 get.getFamilyMap().get(fam2), now - ttl, now, null); 065 ExtendedCell kv = new KeyValue(row1, fam2, col2, 1, data); 066 ExtendedCell cell = PrivateCellUtil.createLastOnRowCol(kv); 067 qm.setToNewRow(kv); 068 MatchCode code = qm.match(cell); 069 assertFalse(code.compareTo(MatchCode.SEEK_NEXT_COL) != 0); 070 } 071 072 @Test 073 public void testMatchExplicitColumns() throws IOException { 074 // Moving up from the Tracker by using Gets and List<KeyValue> instead 075 // of just byte [] 076 077 // Expected result 078 List<MatchCode> expected = new ArrayList<>(6); 079 expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL); 080 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL); 081 expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL); 082 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL); 083 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW); 084 expected.add(ScanQueryMatcher.MatchCode.DONE); 085 086 long now = EnvironmentEdgeManager.currentTime(); 087 // 2,4,5 088 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 089 scan, new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, 090 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 091 get.getFamilyMap().get(fam2), now - ttl, now, null); 092 093 List<KeyValue> memstore = new ArrayList<>(6); 094 memstore.add(new KeyValue(row1, fam2, col1, 1, data)); 095 memstore.add(new KeyValue(row1, fam2, col2, 1, data)); 096 memstore.add(new KeyValue(row1, fam2, col3, 1, data)); 097 memstore.add(new KeyValue(row1, fam2, col4, 1, data)); 098 memstore.add(new KeyValue(row1, fam2, col5, 1, data)); 099 100 memstore.add(new KeyValue(row2, fam1, col1, data)); 101 102 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size()); 103 KeyValue k = memstore.get(0); 104 qm.setToNewRow(k); 105 106 for (KeyValue kv : memstore) { 107 actual.add(qm.match(kv)); 108 } 109 110 assertEquals(expected.size(), actual.size()); 111 for (int i = 0; i < expected.size(); i++) { 112 LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i)); 113 assertEquals(expected.get(i), actual.get(i)); 114 } 115 } 116 117 @Test 118 public void testMatch_Wildcard() throws IOException { 119 // Moving up from the Tracker by using Gets and List<KeyValue> instead 120 // of just byte [] 121 122 // Expected result 123 List<MatchCode> expected = new ArrayList<>(6); 124 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 125 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 126 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 127 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 128 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 129 expected.add(ScanQueryMatcher.MatchCode.DONE); 130 131 long now = EnvironmentEdgeManager.currentTime(); 132 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam2, 0, 1, 133 ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 134 now - ttl, now, null); 135 136 List<KeyValue> memstore = new ArrayList<>(6); 137 memstore.add(new KeyValue(row1, fam2, col1, 1, data)); 138 memstore.add(new KeyValue(row1, fam2, col2, 1, data)); 139 memstore.add(new KeyValue(row1, fam2, col3, 1, data)); 140 memstore.add(new KeyValue(row1, fam2, col4, 1, data)); 141 memstore.add(new KeyValue(row1, fam2, col5, 1, data)); 142 memstore.add(new KeyValue(row2, fam1, col1, 1, data)); 143 144 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size()); 145 146 KeyValue k = memstore.get(0); 147 qm.setToNewRow(k); 148 149 for (KeyValue kv : memstore) { 150 actual.add(qm.match(kv)); 151 } 152 153 assertEquals(expected.size(), actual.size()); 154 for (int i = 0; i < expected.size(); i++) { 155 LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i)); 156 assertEquals(expected.get(i), actual.get(i)); 157 } 158 } 159 160 /** 161 * Verify that {@link ScanQueryMatcher} only skips expired KeyValue instances and does not exit 162 * early from the row (skipping later non-expired KeyValues). This version mimics a Get with 163 * explicitly specified column qualifiers. 164 */ 165 @Test 166 public void testMatch_ExpiredExplicit() throws IOException { 167 168 long testTTL = 1000; 169 MatchCode[] expected = new MatchCode[] { ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, 170 ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL, 171 ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, 172 ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL, 173 ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW, ScanQueryMatcher.MatchCode.DONE }; 174 175 long now = EnvironmentEdgeManager.currentTime(); 176 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, 177 new ScanInfo(this.conf, fam2, 0, 1, testTTL, KeepDeletedCells.FALSE, 178 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 179 get.getFamilyMap().get(fam2), now - testTTL, now, null); 180 181 KeyValue[] kvs = new KeyValue[] { new KeyValue(row1, fam2, col1, now - 100, data), 182 new KeyValue(row1, fam2, col2, now - 50, data), 183 new KeyValue(row1, fam2, col3, now - 5000, data), 184 new KeyValue(row1, fam2, col4, now - 500, data), 185 new KeyValue(row1, fam2, col5, now - 10000, data), 186 new KeyValue(row2, fam1, col1, now - 10, data) }; 187 188 KeyValue k = kvs[0]; 189 qm.setToNewRow(k); 190 191 List<MatchCode> actual = new ArrayList<>(kvs.length); 192 for (KeyValue kv : kvs) { 193 actual.add(qm.match(kv)); 194 } 195 196 assertEquals(expected.length, actual.size()); 197 for (int i = 0; i < expected.length; i++) { 198 LOG.debug("expected " + expected[i] + ", actual " + actual.get(i)); 199 assertEquals(expected[i], actual.get(i)); 200 } 201 } 202 203 /** 204 * Verify that {@link ScanQueryMatcher} only skips expired KeyValue instances and does not exit 205 * early from the row (skipping later non-expired KeyValues). This version mimics a Get with 206 * wildcard-inferred column qualifiers. 207 */ 208 @Test 209 public void testMatch_ExpiredWildcard() throws IOException { 210 211 long testTTL = 1000; 212 MatchCode[] expected = 213 new MatchCode[] { ScanQueryMatcher.MatchCode.INCLUDE, ScanQueryMatcher.MatchCode.INCLUDE, 214 ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, ScanQueryMatcher.MatchCode.INCLUDE, 215 ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, ScanQueryMatcher.MatchCode.DONE }; 216 217 long now = EnvironmentEdgeManager.currentTime(); 218 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam2, 0, 1, 219 testTTL, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 220 now - testTTL, now, null); 221 222 KeyValue[] kvs = new KeyValue[] { new KeyValue(row1, fam2, col1, now - 100, data), 223 new KeyValue(row1, fam2, col2, now - 50, data), 224 new KeyValue(row1, fam2, col3, now - 5000, data), 225 new KeyValue(row1, fam2, col4, now - 500, data), 226 new KeyValue(row1, fam2, col5, now - 10000, data), 227 new KeyValue(row2, fam1, col1, now - 10, data) }; 228 KeyValue k = kvs[0]; 229 qm.setToNewRow(k); 230 231 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(kvs.length); 232 for (KeyValue kv : kvs) { 233 actual.add(qm.match(kv)); 234 } 235 236 assertEquals(expected.length, actual.size()); 237 for (int i = 0; i < expected.length; i++) { 238 LOG.debug("expected " + expected[i] + ", actual " + actual.get(i)); 239 assertEquals(expected[i], actual.get(i)); 240 } 241 } 242 243 private static class AlwaysIncludeAndSeekNextRowFilter extends FilterBase { 244 @Override 245 public ReturnCode filterCell(final Cell c) { 246 return ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW; 247 } 248 } 249 250 @Test 251 public void testMatchWhenFilterReturnsIncludeAndSeekNextRow() throws IOException { 252 List<MatchCode> expected = new ArrayList<>(); 253 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW); 254 expected.add(ScanQueryMatcher.MatchCode.DONE); 255 256 Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeAndSeekNextRowFilter()); 257 258 long now = EnvironmentEdgeManager.currentTime(); 259 260 // scan with column 2,4,5 261 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 262 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, 263 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 264 get.getFamilyMap().get(fam2), now - ttl, now, null); 265 266 List<KeyValue> memstore = new ArrayList<>(); 267 // ColumnTracker will return INCLUDE_AND_SEEK_NEXT_COL , and filter will return 268 // INCLUDE_AND_SEEK_NEXT_ROW, so final match code will be INCLUDE_AND_SEEK_NEXT_ROW. 269 memstore.add(new KeyValue(row1, fam2, col2, 1, data)); 270 memstore.add(new KeyValue(row2, fam1, col1, data)); 271 272 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size()); 273 KeyValue k = memstore.get(0); 274 qm.setToNewRow(k); 275 276 for (KeyValue kv : memstore) { 277 actual.add(qm.match(kv)); 278 } 279 280 assertEquals(expected.size(), actual.size()); 281 for (int i = 0; i < expected.size(); i++) { 282 LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i)); 283 assertEquals(expected.get(i), actual.get(i)); 284 } 285 } 286 287 private static class AlwaysIncludeFilter extends FilterBase { 288 @Override 289 public ReturnCode filterCell(final Cell c) { 290 return ReturnCode.INCLUDE; 291 } 292 } 293 294 private static class FixedSkipHintFilter extends FilterBase { 295 final ExtendedCell hint; 296 297 FixedSkipHintFilter(ExtendedCell hint) { 298 this.hint = hint; 299 } 300 301 @Override 302 public Cell getSkipHint(Cell skippedCell) throws IOException { 303 return hint; 304 } 305 } 306 307 /** 308 * Here is the unit test for UserScanQueryMatcher#mergeFilterResponse, when the number of cells 309 * exceed the versions requested in scan, we should return SEEK_NEXT_COL, but if current match 310 * code is INCLUDE_AND_SEEK_NEXT_ROW, we can optimize to choose the max step between SEEK_NEXT_COL 311 * and INCLUDE_AND_SEEK_NEXT_ROW, which is SEEK_NEXT_ROW. <br/> 312 */ 313 @Test 314 public void testMergeFilterResponseCase1() throws IOException { 315 List<MatchCode> expected = new ArrayList<>(); 316 expected.add(MatchCode.INCLUDE); 317 expected.add(MatchCode.INCLUDE); 318 expected.add(MatchCode.SEEK_NEXT_ROW); 319 320 Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(2); 321 322 long now = EnvironmentEdgeManager.currentTime(); 323 // scan with column 2,4,5, the family with maxVersion = 3 324 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 325 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 3, ttl, KeepDeletedCells.FALSE, 326 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 327 get.getFamilyMap().get(fam2), now - ttl, now, null); 328 329 List<KeyValue> memstore = new ArrayList<>(); 330 memstore.add(new KeyValue(row1, fam1, col5, 1, data)); // match code will be INCLUDE 331 memstore.add(new KeyValue(row1, fam1, col5, 2, data)); // match code will be INCLUDE 332 333 // match code will be SEEK_NEXT_ROW , which is max(INCLUDE_AND_SEEK_NEXT_ROW, SEEK_NEXT_COL). 334 memstore.add(new KeyValue(row1, fam1, col5, 3, data)); 335 336 KeyValue k = memstore.get(0); 337 qm.setToNewRow(k); 338 339 for (int i = 0; i < memstore.size(); i++) { 340 assertEquals(expected.get(i), qm.match(memstore.get(i))); 341 } 342 343 scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(1); 344 qm = UserScanQueryMatcher.create( 345 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 2, ttl, KeepDeletedCells.FALSE, 346 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 347 get.getFamilyMap().get(fam2), now - ttl, now, null); 348 349 List<KeyValue> memstore2 = new ArrayList<>(); 350 memstore2.add(new KeyValue(row2, fam1, col2, 1, data)); // match code will be INCLUDE 351 // match code will be SEEK_NEXT_COL, which is max(INCLUDE_AND_SEEK_NEXT_COL, SEEK_NEXT_COL). 352 memstore2.add(new KeyValue(row2, fam1, col2, 2, data)); 353 354 k = memstore2.get(0); 355 qm.setToNewRow(k); 356 357 assertEquals(MatchCode.INCLUDE, qm.match(memstore2.get(0))); 358 assertEquals(MatchCode.SEEK_NEXT_COL, qm.match(memstore2.get(1))); 359 } 360 361 /** 362 * Here is the unit test for UserScanQueryMatcher#mergeFilterResponse: the match code may be 363 * changed to SEEK_NEXT_COL or INCLUDE_AND_SEEK_NEXT_COL after merging with filterResponse, even 364 * if the passed match code is neither SEEK_NEXT_COL nor INCLUDE_AND_SEEK_NEXT_COL. In that case, 365 * we need to make sure that the ColumnTracker has been switched to the next column. <br/> 366 * An effective test way is: we only need to check the cell from getKeyForNextColumn(). because 367 * that as long as the UserScanQueryMatcher returns SEEK_NEXT_COL or INCLUDE_AND_SEEK_NEXT_COL, 368 * UserScanQueryMatcher#getKeyForNextColumn should return an cell whose column is larger than the 369 * current cell's. 370 */ 371 @Test 372 public void testMergeFilterResponseCase2() throws Exception { 373 List<MatchCode> expected = new ArrayList<>(); 374 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 375 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 376 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 377 expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL); 378 379 Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(3); 380 381 long now = EnvironmentEdgeManager.currentTime(); 382 383 // scan with column 2,4,5, the family with maxVersion = 5 384 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 385 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 5, ttl, KeepDeletedCells.FALSE, 386 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 387 get.getFamilyMap().get(fam2), now - ttl, now, null); 388 389 List<KeyValue> memstore = new ArrayList<>(); 390 391 memstore.add(new KeyValue(row1, fam1, col2, 1, data)); // match code will be INCLUDE 392 memstore.add(new KeyValue(row1, fam1, col2, 2, data)); // match code will be INCLUDE 393 memstore.add(new KeyValue(row1, fam1, col2, 3, data)); // match code will be INCLUDE 394 memstore.add(new KeyValue(row1, fam1, col2, 4, data)); // match code will be SEEK_NEXT_COL 395 396 KeyValue k = memstore.get(0); 397 qm.setToNewRow(k); 398 399 for (int i = 0; i < memstore.size(); i++) { 400 assertEquals(expected.get(i), qm.match(memstore.get(i))); 401 } 402 403 // For last cell, the query matcher will return SEEK_NEXT_COL, and the 404 // ColumnTracker will skip to the next column, which is col4. 405 ExtendedCell lastCell = memstore.get(memstore.size() - 1); 406 Cell nextCell = qm.getKeyForNextColumn(lastCell); 407 assertArrayEquals(nextCell.getQualifierArray(), col4); 408 } 409 410 /** Verify that getSkipHint is consulted when a cell is newer than the time-range upper bound. */ 411 @Test 412 public void testSkipHintConsultedForCellNewerThanTimeRange() throws IOException { 413 long now = EnvironmentEdgeManager.currentTime(); 414 long minTs = now - 2000; 415 long maxTs = now - 1000; 416 417 KeyValue hintCell = new KeyValue(row2, fam2, col1, now - 1500, data); 418 Scan timeRangeScan = new Scan().addFamily(fam2).setTimeRange(minTs, maxTs) 419 .setFilter(new FixedSkipHintFilter(hintCell)); 420 421 long now2 = EnvironmentEdgeManager.currentTime(); 422 UserScanQueryMatcher qm = UserScanQueryMatcher.create(timeRangeScan, 423 new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 424 0, rowComparator, false), 425 null, 0, now2, null); 426 427 // ts=now2 >= maxTs, so tsCmp > 0: time-range gate fires before filterCell. 428 KeyValue tooNew = new KeyValue(row1, fam2, col1, now2, data); 429 qm.setToNewRow(tooNew); 430 431 assertEquals(MatchCode.SEEK_NEXT_USING_HINT, qm.match(tooNew)); 432 assertEquals(hintCell, qm.getNextKeyHint(tooNew)); 433 assertNull(qm.getNextKeyHint(tooNew)); 434 } 435 436 /** Verify that getSkipHint is consulted when a cell is older than the time-range lower bound. */ 437 @Test 438 public void testSkipHintConsultedForCellOlderThanTimeRange() throws IOException { 439 long now = EnvironmentEdgeManager.currentTime(); 440 long minTs = now - 500; 441 long maxTs = now; 442 443 KeyValue hintCell = new KeyValue(row2, fam2, col1, now - 100, data); 444 Scan timeRangeScan = new Scan().addFamily(fam2).setTimeRange(minTs, maxTs) 445 .setFilter(new FixedSkipHintFilter(hintCell)); 446 447 UserScanQueryMatcher qm = UserScanQueryMatcher.create(timeRangeScan, 448 new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 449 0, rowComparator, false), 450 null, 0, now, null); 451 452 // ts = now-1000 < minTs (now-500), so tsCmp < 0: time-range gate fires. 453 KeyValue tooOld = new KeyValue(row1, fam2, col1, now - 1000, data); 454 qm.setToNewRow(tooOld); 455 456 assertEquals(MatchCode.SEEK_NEXT_USING_HINT, qm.match(tooOld)); 457 assertEquals(hintCell, qm.getNextKeyHint(tooOld)); 458 assertNull(qm.getNextKeyHint(tooOld)); 459 } 460 461 /** Verify that a null getSkipHint falls back to the original SKIP match code. */ 462 @Test 463 public void testNullSkipHintFallsThroughToOriginalCodeOnTimeRangeGate() throws IOException { 464 long now = EnvironmentEdgeManager.currentTime(); 465 long minTs = now - 2000; 466 long maxTs = now - 1000; 467 468 Scan timeRangeScan = 469 new Scan().addFamily(fam2).setTimeRange(minTs, maxTs).setFilter(new AlwaysIncludeFilter()); 470 471 long now2 = EnvironmentEdgeManager.currentTime(); 472 UserScanQueryMatcher qm = UserScanQueryMatcher.create(timeRangeScan, 473 new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 474 0, rowComparator, false), 475 null, 0, now2, null); 476 477 KeyValue tooNew = new KeyValue(row1, fam2, col1, now2, data); 478 qm.setToNewRow(tooNew); 479 480 assertEquals(MatchCode.SKIP, qm.match(tooNew)); 481 } 482 483 /** Verify that getSkipHint is consulted when a column is excluded by the scan's column set. */ 484 @Test 485 public void testSkipHintConsultedForExcludedColumn() throws IOException { 486 long now = EnvironmentEdgeManager.currentTime(); 487 488 // get.getFamilyMap().get(fam2) contains col2, col4, col5; presenting col1 triggers exclusion. 489 KeyValue hintCell = new KeyValue(row1, fam2, col2, 1, data); 490 Scan scanWithFilter = new Scan(scan).setFilter(new FixedSkipHintFilter(hintCell)); 491 492 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 493 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, 494 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 495 get.getFamilyMap().get(fam2), now - ttl, now, null); 496 497 // col1 is not in {col2, col4, col5}: checkColumn returns a non-INCLUDE code. 498 KeyValue excludedCol = new KeyValue(row1, fam2, col1, 1, data); 499 qm.setToNewRow(excludedCol); 500 501 assertEquals(MatchCode.SEEK_NEXT_USING_HINT, qm.match(excludedCol)); 502 assertEquals(hintCell, qm.getNextKeyHint(excludedCol)); 503 assertNull(qm.getNextKeyHint(excludedCol)); 504 } 505 506 /** Verify that getSkipHint is consulted when the version limit is exhausted. */ 507 @Test 508 public void testSkipHintConsultedOnVersionExhaustion() throws IOException { 509 long now = EnvironmentEdgeManager.currentTime(); 510 511 KeyValue hintCell = new KeyValue(row2, fam2, col1, now, data); 512 Scan scanWithFilter = new Scan().addFamily(fam2).setFilter(new FixedSkipHintFilter(hintCell)); 513 514 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scanWithFilter, 515 new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 516 0, rowComparator, false), 517 null, now - ttl, now, null); 518 519 KeyValue version1 = new KeyValue(row1, fam2, col1, now - 10, data); 520 qm.setToNewRow(version1); 521 assertEquals(MatchCode.INCLUDE, qm.match(version1)); 522 523 // Second version: version limit exceeded, checkVersions returns SEEK_NEXT_COL. 524 KeyValue version2 = new KeyValue(row1, fam2, col1, now - 20, data); 525 assertEquals(MatchCode.SEEK_NEXT_USING_HINT, qm.match(version2)); 526 assertEquals(hintCell, qm.getNextKeyHint(version2)); 527 assertNull(qm.getNextKeyHint(version2)); 528 } 529 530 /** Verify that pendingSkipHint is cleared when a row transition occurs via setToNewRow. */ 531 @Test 532 public void testPendingSkipHintClearedOnRowTransition() throws IOException { 533 long now = EnvironmentEdgeManager.currentTime(); 534 535 KeyValue hintCell = new KeyValue(row2, fam2, col1, now, data); 536 Scan scanWithFilter = new Scan().addFamily(fam2).setFilter(new FixedSkipHintFilter(hintCell)); 537 538 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scanWithFilter, 539 new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 540 0, rowComparator, false), 541 null, now - ttl, now, null); 542 543 // Trigger a structural skip that stores pendingSkipHint. 544 KeyValue version1 = new KeyValue(row1, fam2, col1, now - 10, data); 545 qm.setToNewRow(version1); 546 assertEquals(MatchCode.INCLUDE, qm.match(version1)); 547 KeyValue version2 = new KeyValue(row1, fam2, col1, now - 20, data); 548 assertEquals(MatchCode.SEEK_NEXT_USING_HINT, qm.match(version2)); 549 550 // Do NOT consume the hint via getNextKeyHint. Instead, simulate a row transition. 551 KeyValue newRowCell = new KeyValue(row2, fam2, col1, now - 10, data); 552 qm.setToNewRow(newRowCell); 553 554 // After row transition, pendingSkipHint must be null — getNextKeyHint should delegate 555 // to the filter's getNextCellHint (which returns null for FixedSkipHintFilter). 556 assertNull(qm.getNextKeyHint(newRowCell)); 557 } 558 559 /** Verify that the normal filterCell/getNextCellHint path is unaffected by pendingSkipHint. */ 560 @Test 561 public void testNormalFilterCellHintPathUnaffectedBySkipHintChange() throws IOException { 562 long now = EnvironmentEdgeManager.currentTime(); 563 564 final KeyValue filterHintCell = new KeyValue(row2, fam2, col1, now, data); 565 566 FilterBase seekFilter = new FilterBase() { 567 @Override 568 public ReturnCode filterCell(Cell c) { 569 return ReturnCode.SEEK_NEXT_USING_HINT; 570 } 571 572 @Override 573 public Cell getNextCellHint(Cell currentCell) { 574 return filterHintCell; 575 } 576 }; 577 578 Scan scanWithFilter = new Scan().addFamily(fam2).setFilter(seekFilter); 579 580 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scanWithFilter, 581 new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 582 0, rowComparator, false), 583 null, now - ttl, now, null); 584 585 KeyValue cell = new KeyValue(row1, fam2, col1, now - 10, data); 586 qm.setToNewRow(cell); 587 588 assertEquals(MatchCode.SEEK_NEXT_USING_HINT, qm.match(cell)); 589 assertEquals(filterHintCell, qm.getNextKeyHint(cell)); 590 } 591 592 /** Verify that getSkipHint works correctly for reversed scans (hint must be smaller). */ 593 @Test 594 public void testSkipHintConsultedForReversedScan() throws IOException { 595 long now = EnvironmentEdgeManager.currentTime(); 596 long minTs = now - 2000; 597 long maxTs = now - 1000; 598 599 // For reversed scan, the hint must point backward (smaller key). 600 KeyValue hintCell = new KeyValue(row1, fam2, col1, now - 1500, data); 601 Scan reversedScan = new Scan().addFamily(fam2).setTimeRange(minTs, maxTs).setReversed(true) 602 .setFilter(new FixedSkipHintFilter(hintCell)); 603 604 long now2 = EnvironmentEdgeManager.currentTime(); 605 UserScanQueryMatcher qm = UserScanQueryMatcher.create(reversedScan, 606 new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 607 0, rowComparator, false), 608 null, 0, now2, null); 609 610 // ts=now2 >= maxTs so tsCmp > 0: time-range gate fires. 611 // row2 > row1 in natural order, so for reversed scan hint (row1) is "forward" (smaller). 612 KeyValue tooNew = new KeyValue(row2, fam2, col1, now2, data); 613 qm.setToNewRow(tooNew); 614 615 assertEquals(MatchCode.SEEK_NEXT_USING_HINT, qm.match(tooNew)); 616 assertEquals(hintCell, qm.getNextKeyHint(tooNew)); 617 assertNull(qm.getNextKeyHint(tooNew)); 618 } 619 620 /** 621 * After enough consecutive range delete markers, the matcher should switch from SKIP to 622 * SEEK_NEXT_COL. Point deletes and KEEP_DELETED_CELLS always SKIP. 623 */ 624 @Test 625 public void testSeekOnRangeDelete() throws IOException { 626 int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD; 627 628 // DeleteColumn: first N-1 SKIP, N-th triggers SEEK_NEXT_COL 629 assertSeekAfterThreshold(KeepDeletedCells.FALSE, Type.DeleteColumn, n); 630 631 // DeleteFamily: same threshold behavior 632 assertSeekAfterThreshold(KeepDeletedCells.FALSE, Type.DeleteFamily, n); 633 634 // Delete (version): always SKIP (point delete, not range) 635 assertAllSkip(KeepDeletedCells.FALSE, Type.Delete, n + 1); 636 637 // KEEP_DELETED_CELLS=TRUE: always SKIP 638 assertAllSkip(KeepDeletedCells.TRUE, Type.DeleteColumn, n + 1); 639 } 640 641 /** 642 * DeleteColumn with empty qualifier must not cause seeking past a subsequent DeleteFamily. 643 * DeleteFamily masks all columns, so it must be tracked by the delete tracker. 644 */ 645 @Test 646 public void testDeleteColumnEmptyQualifierDoesNotSkipDeleteFamily() throws IOException { 647 long now = EnvironmentEdgeManager.currentTime(); 648 byte[] e = HConstants.EMPTY_BYTE_ARRAY; 649 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1, 650 ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 651 now - ttl, now, null); 652 653 int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD; 654 // Feed DCs with empty qualifier past the threshold, then a DF. 655 // The DF must NOT be seeked past -- it must be SKIP'd so the tracker picks it up. 656 qm.setToNewRow(new KeyValue(row1, fam1, e, now, Type.DeleteColumn)); 657 for (int i = 0; i < n + 1; i++) { 658 // Empty qualifier DCs should never trigger seek, regardless of threshold 659 assertEquals(MatchCode.SKIP, 660 qm.match(new KeyValue(row1, fam1, e, now - i, Type.DeleteColumn)), "DC at i=" + i); 661 } 662 KeyValue df = new KeyValue(row1, fam1, e, now - n - 1, Type.DeleteFamily); 663 KeyValue put = new KeyValue(row1, fam1, col1, now - n - 1, Type.Put, data); 664 // DF must be processed (SKIP), not seeked past 665 assertEquals(MatchCode.SKIP, qm.match(df)); 666 // Put in col1 at t=now-3 should be masked by DF@t=now-3 667 MatchCode putCode = qm.match(put); 668 assertEquals(MatchCode.SEEK_NEXT_COL, putCode); 669 } 670 671 /** 672 * DeleteColumn markers for different qualifiers should not accumulate the seek counter. Only 673 * consecutive markers for the same qualifier should trigger seeking. 674 */ 675 @Test 676 public void testDeleteColumnDifferentQualifiersDoNotSeek() throws IOException { 677 long now = EnvironmentEdgeManager.currentTime(); 678 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1, 679 ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 680 now - ttl, now, null); 681 682 // DCs for different qualifiers: counter resets on qualifier change, never seeks 683 qm.setToNewRow(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn)); 684 assertEquals(MatchCode.SKIP, qm.match(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn))); 685 assertEquals(MatchCode.SKIP, 686 qm.match(new KeyValue(row1, fam1, col2, now - 1, Type.DeleteColumn))); 687 assertEquals(MatchCode.SKIP, 688 qm.match(new KeyValue(row1, fam1, col3, now - 2, Type.DeleteColumn))); 689 assertEquals(MatchCode.SKIP, 690 qm.match(new KeyValue(row1, fam1, col4, now - 3, Type.DeleteColumn))); 691 assertEquals(MatchCode.SKIP, 692 qm.match(new KeyValue(row1, fam1, col5, now - 4, Type.DeleteColumn))); 693 } 694 695 /** 696 * Delete markers outside the scan's time range (includeDeleteMarker=false) should still 697 * accumulate the seek counter and trigger SEEK_NEXT_COL after the threshold. 698 */ 699 @Test 700 public void testSeekOnRangeDeleteOutsideTimeRange() throws IOException { 701 long now = EnvironmentEdgeManager.currentTime(); 702 long futureTs = now + 1_000_000; 703 Scan scanWithTimeRange = new Scan(scan).setTimeRange(futureTs, Long.MAX_VALUE); 704 705 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scanWithTimeRange, 706 new ScanInfo(this.conf, fam1, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 707 0, rowComparator, false), 708 null, now - ttl, now, null); 709 710 int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD; 711 qm.setToNewRow(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn)); 712 // All DCs have timestamps below the time range, so includeDeleteMarker is false. 713 // The seek counter should still accumulate. 714 for (int i = 0; i < n - 1; i++) { 715 assertEquals(MatchCode.SKIP, 716 qm.match(new KeyValue(row1, fam1, col1, now - i, Type.DeleteColumn)), "DC at i=" + i); 717 } 718 assertEquals(MatchCode.SEEK_NEXT_COL, 719 qm.match(new KeyValue(row1, fam1, col1, now - n + 1, Type.DeleteColumn))); 720 } 721 722 private UserScanQueryMatcher createDeleteMatcher(KeepDeletedCells keepDeletedCells) 723 throws IOException { 724 long now = EnvironmentEdgeManager.currentTime(); 725 return UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1, ttl, 726 keepDeletedCells, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, now - ttl, 727 now, null); 728 } 729 730 /** First n-1 markers SKIP, n-th triggers SEEK_NEXT_COL. */ 731 private void assertSeekAfterThreshold(KeepDeletedCells keepDeletedCells, Type type, int n) 732 throws IOException { 733 long now = EnvironmentEdgeManager.currentTime(); 734 UserScanQueryMatcher qm = createDeleteMatcher(keepDeletedCells); 735 boolean familyLevel = type == Type.DeleteFamily || type == Type.DeleteFamilyVersion; 736 byte[] qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1; 737 qm.setToNewRow(new KeyValue(row1, fam1, qual, now, type)); 738 for (int i = 0; i < n - 1; i++) { 739 assertEquals(MatchCode.SKIP, qm.match(new KeyValue(row1, fam1, qual, now - i, type)), 740 "Mismatch at index " + i); 741 } 742 assertEquals(MatchCode.SEEK_NEXT_COL, 743 qm.match(new KeyValue(row1, fam1, qual, now - n + 1, type)), 744 "Expected SEEK_NEXT_COL at index " + (n - 1)); 745 } 746 747 /** All markers should SKIP regardless of count. */ 748 private void assertAllSkip(KeepDeletedCells keepDeletedCells, Type type, int count) 749 throws IOException { 750 long now = EnvironmentEdgeManager.currentTime(); 751 UserScanQueryMatcher qm = createDeleteMatcher(keepDeletedCells); 752 boolean familyLevel = type == Type.DeleteFamily || type == Type.DeleteFamilyVersion; 753 byte[] qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1; 754 qm.setToNewRow(new KeyValue(row1, fam1, qual, now, type)); 755 for (int i = 0; i < count; i++) { 756 assertEquals(MatchCode.SKIP, qm.match(new KeyValue(row1, fam1, qual, now - i, type)), 757 "Mismatch at index " + i); 758 } 759 } 760}