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.Assert.assertArrayEquals; 021import static org.junit.Assert.assertEquals; 022import static org.junit.Assert.assertFalse; 023 024import java.io.IOException; 025import java.util.ArrayList; 026import java.util.List; 027import org.apache.hadoop.hbase.Cell; 028import org.apache.hadoop.hbase.ExtendedCell; 029import org.apache.hadoop.hbase.HBaseClassTestRule; 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.ClassRule; 043import org.junit.Test; 044import org.junit.experimental.categories.Category; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047 048@Category({ RegionServerTests.class, SmallTests.class }) 049public class TestUserScanQueryMatcher extends AbstractTestScanQueryMatcher { 050 051 @ClassRule 052 public static final HBaseClassTestRule CLASS_RULE = 053 HBaseClassTestRule.forClass(TestUserScanQueryMatcher.class); 054 055 private static final Logger LOG = LoggerFactory.getLogger(TestUserScanQueryMatcher.class); 056 057 /** 058 * This is a cryptic test. It is checking that we don't include a fake cell. See HBASE-16074 for 059 * background. 060 */ 061 @Test 062 public void testNeverIncludeFakeCell() throws IOException { 063 long now = EnvironmentEdgeManager.currentTime(); 064 // Do with fam2 which has a col2 qualifier. 065 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, 066 new ScanInfo(this.conf, fam2, 10, 1, ttl, KeepDeletedCells.FALSE, 067 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 068 get.getFamilyMap().get(fam2), now - ttl, now, null); 069 ExtendedCell kv = new KeyValue(row1, fam2, col2, 1, data); 070 ExtendedCell cell = PrivateCellUtil.createLastOnRowCol(kv); 071 qm.setToNewRow(kv); 072 MatchCode code = qm.match(cell); 073 assertFalse(code.compareTo(MatchCode.SEEK_NEXT_COL) != 0); 074 } 075 076 @Test 077 public void testMatchExplicitColumns() throws IOException { 078 // Moving up from the Tracker by using Gets and List<KeyValue> instead 079 // of just byte [] 080 081 // Expected result 082 List<MatchCode> expected = new ArrayList<>(6); 083 expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL); 084 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL); 085 expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL); 086 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL); 087 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW); 088 expected.add(ScanQueryMatcher.MatchCode.DONE); 089 090 long now = EnvironmentEdgeManager.currentTime(); 091 // 2,4,5 092 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 093 scan, new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, 094 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 095 get.getFamilyMap().get(fam2), now - ttl, now, null); 096 097 List<KeyValue> memstore = new ArrayList<>(6); 098 memstore.add(new KeyValue(row1, fam2, col1, 1, data)); 099 memstore.add(new KeyValue(row1, fam2, col2, 1, data)); 100 memstore.add(new KeyValue(row1, fam2, col3, 1, data)); 101 memstore.add(new KeyValue(row1, fam2, col4, 1, data)); 102 memstore.add(new KeyValue(row1, fam2, col5, 1, data)); 103 104 memstore.add(new KeyValue(row2, fam1, col1, data)); 105 106 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size()); 107 KeyValue k = memstore.get(0); 108 qm.setToNewRow(k); 109 110 for (KeyValue kv : memstore) { 111 actual.add(qm.match(kv)); 112 } 113 114 assertEquals(expected.size(), actual.size()); 115 for (int i = 0; i < expected.size(); i++) { 116 LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i)); 117 assertEquals(expected.get(i), actual.get(i)); 118 } 119 } 120 121 @Test 122 public void testMatch_Wildcard() throws IOException { 123 // Moving up from the Tracker by using Gets and List<KeyValue> instead 124 // of just byte [] 125 126 // Expected result 127 List<MatchCode> expected = new ArrayList<>(6); 128 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 129 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 130 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 131 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 132 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 133 expected.add(ScanQueryMatcher.MatchCode.DONE); 134 135 long now = EnvironmentEdgeManager.currentTime(); 136 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam2, 0, 1, 137 ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 138 now - ttl, now, null); 139 140 List<KeyValue> memstore = new ArrayList<>(6); 141 memstore.add(new KeyValue(row1, fam2, col1, 1, data)); 142 memstore.add(new KeyValue(row1, fam2, col2, 1, data)); 143 memstore.add(new KeyValue(row1, fam2, col3, 1, data)); 144 memstore.add(new KeyValue(row1, fam2, col4, 1, data)); 145 memstore.add(new KeyValue(row1, fam2, col5, 1, data)); 146 memstore.add(new KeyValue(row2, fam1, col1, 1, data)); 147 148 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size()); 149 150 KeyValue k = memstore.get(0); 151 qm.setToNewRow(k); 152 153 for (KeyValue kv : memstore) { 154 actual.add(qm.match(kv)); 155 } 156 157 assertEquals(expected.size(), actual.size()); 158 for (int i = 0; i < expected.size(); i++) { 159 LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i)); 160 assertEquals(expected.get(i), actual.get(i)); 161 } 162 } 163 164 /** 165 * Verify that {@link ScanQueryMatcher} only skips expired KeyValue instances and does not exit 166 * early from the row (skipping later non-expired KeyValues). This version mimics a Get with 167 * explicitly specified column qualifiers. 168 */ 169 @Test 170 public void testMatch_ExpiredExplicit() throws IOException { 171 172 long testTTL = 1000; 173 MatchCode[] expected = new MatchCode[] { ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, 174 ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL, 175 ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, 176 ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL, 177 ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW, ScanQueryMatcher.MatchCode.DONE }; 178 179 long now = EnvironmentEdgeManager.currentTime(); 180 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, 181 new ScanInfo(this.conf, fam2, 0, 1, testTTL, KeepDeletedCells.FALSE, 182 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 183 get.getFamilyMap().get(fam2), now - testTTL, now, null); 184 185 KeyValue[] kvs = new KeyValue[] { new KeyValue(row1, fam2, col1, now - 100, data), 186 new KeyValue(row1, fam2, col2, now - 50, data), 187 new KeyValue(row1, fam2, col3, now - 5000, data), 188 new KeyValue(row1, fam2, col4, now - 500, data), 189 new KeyValue(row1, fam2, col5, now - 10000, data), 190 new KeyValue(row2, fam1, col1, now - 10, data) }; 191 192 KeyValue k = kvs[0]; 193 qm.setToNewRow(k); 194 195 List<MatchCode> actual = new ArrayList<>(kvs.length); 196 for (KeyValue kv : kvs) { 197 actual.add(qm.match(kv)); 198 } 199 200 assertEquals(expected.length, actual.size()); 201 for (int i = 0; i < expected.length; i++) { 202 LOG.debug("expected " + expected[i] + ", actual " + actual.get(i)); 203 assertEquals(expected[i], actual.get(i)); 204 } 205 } 206 207 /** 208 * Verify that {@link ScanQueryMatcher} only skips expired KeyValue instances and does not exit 209 * early from the row (skipping later non-expired KeyValues). This version mimics a Get with 210 * wildcard-inferred column qualifiers. 211 */ 212 @Test 213 public void testMatch_ExpiredWildcard() throws IOException { 214 215 long testTTL = 1000; 216 MatchCode[] expected = 217 new MatchCode[] { ScanQueryMatcher.MatchCode.INCLUDE, ScanQueryMatcher.MatchCode.INCLUDE, 218 ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, ScanQueryMatcher.MatchCode.INCLUDE, 219 ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, ScanQueryMatcher.MatchCode.DONE }; 220 221 long now = EnvironmentEdgeManager.currentTime(); 222 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam2, 0, 1, 223 testTTL, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 224 now - testTTL, now, null); 225 226 KeyValue[] kvs = new KeyValue[] { new KeyValue(row1, fam2, col1, now - 100, data), 227 new KeyValue(row1, fam2, col2, now - 50, data), 228 new KeyValue(row1, fam2, col3, now - 5000, data), 229 new KeyValue(row1, fam2, col4, now - 500, data), 230 new KeyValue(row1, fam2, col5, now - 10000, data), 231 new KeyValue(row2, fam1, col1, now - 10, data) }; 232 KeyValue k = kvs[0]; 233 qm.setToNewRow(k); 234 235 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(kvs.length); 236 for (KeyValue kv : kvs) { 237 actual.add(qm.match(kv)); 238 } 239 240 assertEquals(expected.length, actual.size()); 241 for (int i = 0; i < expected.length; i++) { 242 LOG.debug("expected " + expected[i] + ", actual " + actual.get(i)); 243 assertEquals(expected[i], actual.get(i)); 244 } 245 } 246 247 private static class AlwaysIncludeAndSeekNextRowFilter extends FilterBase { 248 @Override 249 public ReturnCode filterCell(final Cell c) { 250 return ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW; 251 } 252 } 253 254 @Test 255 public void testMatchWhenFilterReturnsIncludeAndSeekNextRow() throws IOException { 256 List<MatchCode> expected = new ArrayList<>(); 257 expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW); 258 expected.add(ScanQueryMatcher.MatchCode.DONE); 259 260 Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeAndSeekNextRowFilter()); 261 262 long now = EnvironmentEdgeManager.currentTime(); 263 264 // scan with column 2,4,5 265 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 266 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE, 267 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 268 get.getFamilyMap().get(fam2), now - ttl, now, null); 269 270 List<KeyValue> memstore = new ArrayList<>(); 271 // ColumnTracker will return INCLUDE_AND_SEEK_NEXT_COL , and filter will return 272 // INCLUDE_AND_SEEK_NEXT_ROW, so final match code will be INCLUDE_AND_SEEK_NEXT_ROW. 273 memstore.add(new KeyValue(row1, fam2, col2, 1, data)); 274 memstore.add(new KeyValue(row2, fam1, col1, data)); 275 276 List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size()); 277 KeyValue k = memstore.get(0); 278 qm.setToNewRow(k); 279 280 for (KeyValue kv : memstore) { 281 actual.add(qm.match(kv)); 282 } 283 284 assertEquals(expected.size(), actual.size()); 285 for (int i = 0; i < expected.size(); i++) { 286 LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i)); 287 assertEquals(expected.get(i), actual.get(i)); 288 } 289 } 290 291 private static class AlwaysIncludeFilter extends FilterBase { 292 @Override 293 public ReturnCode filterCell(final Cell c) { 294 return ReturnCode.INCLUDE; 295 } 296 } 297 298 /** 299 * Here is the unit test for UserScanQueryMatcher#mergeFilterResponse, when the number of cells 300 * exceed the versions requested in scan, we should return SEEK_NEXT_COL, but if current match 301 * code is INCLUDE_AND_SEEK_NEXT_ROW, we can optimize to choose the max step between SEEK_NEXT_COL 302 * and INCLUDE_AND_SEEK_NEXT_ROW, which is SEEK_NEXT_ROW. <br/> 303 */ 304 @Test 305 public void testMergeFilterResponseCase1() throws IOException { 306 List<MatchCode> expected = new ArrayList<>(); 307 expected.add(MatchCode.INCLUDE); 308 expected.add(MatchCode.INCLUDE); 309 expected.add(MatchCode.SEEK_NEXT_ROW); 310 311 Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(2); 312 313 long now = EnvironmentEdgeManager.currentTime(); 314 // scan with column 2,4,5, the family with maxVersion = 3 315 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 316 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 3, ttl, KeepDeletedCells.FALSE, 317 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 318 get.getFamilyMap().get(fam2), now - ttl, now, null); 319 320 List<KeyValue> memstore = new ArrayList<>(); 321 memstore.add(new KeyValue(row1, fam1, col5, 1, data)); // match code will be INCLUDE 322 memstore.add(new KeyValue(row1, fam1, col5, 2, data)); // match code will be INCLUDE 323 324 // match code will be SEEK_NEXT_ROW , which is max(INCLUDE_AND_SEEK_NEXT_ROW, SEEK_NEXT_COL). 325 memstore.add(new KeyValue(row1, fam1, col5, 3, data)); 326 327 KeyValue k = memstore.get(0); 328 qm.setToNewRow(k); 329 330 for (int i = 0; i < memstore.size(); i++) { 331 assertEquals(expected.get(i), qm.match(memstore.get(i))); 332 } 333 334 scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(1); 335 qm = UserScanQueryMatcher.create( 336 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 2, ttl, KeepDeletedCells.FALSE, 337 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 338 get.getFamilyMap().get(fam2), now - ttl, now, null); 339 340 List<KeyValue> memstore2 = new ArrayList<>(); 341 memstore2.add(new KeyValue(row2, fam1, col2, 1, data)); // match code will be INCLUDE 342 // match code will be SEEK_NEXT_COL, which is max(INCLUDE_AND_SEEK_NEXT_COL, SEEK_NEXT_COL). 343 memstore2.add(new KeyValue(row2, fam1, col2, 2, data)); 344 345 k = memstore2.get(0); 346 qm.setToNewRow(k); 347 348 assertEquals(MatchCode.INCLUDE, qm.match(memstore2.get(0))); 349 assertEquals(MatchCode.SEEK_NEXT_COL, qm.match(memstore2.get(1))); 350 } 351 352 /** 353 * Here is the unit test for UserScanQueryMatcher#mergeFilterResponse: the match code may be 354 * changed to SEEK_NEXT_COL or INCLUDE_AND_SEEK_NEXT_COL after merging with filterResponse, even 355 * if the passed match code is neither SEEK_NEXT_COL nor INCLUDE_AND_SEEK_NEXT_COL. In that case, 356 * we need to make sure that the ColumnTracker has been switched to the next column. <br/> 357 * An effective test way is: we only need to check the cell from getKeyForNextColumn(). because 358 * that as long as the UserScanQueryMatcher returns SEEK_NEXT_COL or INCLUDE_AND_SEEK_NEXT_COL, 359 * UserScanQueryMatcher#getKeyForNextColumn should return an cell whose column is larger than the 360 * current cell's. 361 */ 362 @Test 363 public void testMergeFilterResponseCase2() throws Exception { 364 List<MatchCode> expected = new ArrayList<>(); 365 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 366 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 367 expected.add(ScanQueryMatcher.MatchCode.INCLUDE); 368 expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL); 369 370 Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(3); 371 372 long now = EnvironmentEdgeManager.currentTime(); 373 374 // scan with column 2,4,5, the family with maxVersion = 5 375 UserScanQueryMatcher qm = UserScanQueryMatcher.create( 376 scanWithFilter, new ScanInfo(this.conf, fam2, 0, 5, ttl, KeepDeletedCells.FALSE, 377 HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), 378 get.getFamilyMap().get(fam2), now - ttl, now, null); 379 380 List<KeyValue> memstore = new ArrayList<>(); 381 382 memstore.add(new KeyValue(row1, fam1, col2, 1, data)); // match code will be INCLUDE 383 memstore.add(new KeyValue(row1, fam1, col2, 2, data)); // match code will be INCLUDE 384 memstore.add(new KeyValue(row1, fam1, col2, 3, data)); // match code will be INCLUDE 385 memstore.add(new KeyValue(row1, fam1, col2, 4, data)); // match code will be SEEK_NEXT_COL 386 387 KeyValue k = memstore.get(0); 388 qm.setToNewRow(k); 389 390 for (int i = 0; i < memstore.size(); i++) { 391 assertEquals(expected.get(i), qm.match(memstore.get(i))); 392 } 393 394 // For last cell, the query matcher will return SEEK_NEXT_COL, and the 395 // ColumnTracker will skip to the next column, which is col4. 396 ExtendedCell lastCell = memstore.get(memstore.size() - 1); 397 Cell nextCell = qm.getKeyForNextColumn(lastCell); 398 assertArrayEquals(nextCell.getQualifierArray(), col4); 399 } 400 401 /** 402 * After enough consecutive range delete markers, the matcher should switch from SKIP to 403 * SEEK_NEXT_COL. Point deletes and KEEP_DELETED_CELLS always SKIP. 404 */ 405 @Test 406 public void testSeekOnRangeDelete() throws IOException { 407 int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD; 408 409 // DeleteColumn: first N-1 SKIP, N-th triggers SEEK_NEXT_COL 410 assertSeekAfterThreshold(KeepDeletedCells.FALSE, Type.DeleteColumn, n); 411 412 // DeleteFamily: same threshold behavior 413 assertSeekAfterThreshold(KeepDeletedCells.FALSE, Type.DeleteFamily, n); 414 415 // Delete (version): always SKIP (point delete, not range) 416 assertAllSkip(KeepDeletedCells.FALSE, Type.Delete, n + 1); 417 418 // KEEP_DELETED_CELLS=TRUE: always SKIP 419 assertAllSkip(KeepDeletedCells.TRUE, Type.DeleteColumn, n + 1); 420 } 421 422 /** 423 * DeleteColumn with empty qualifier must not cause seeking past a subsequent DeleteFamily. 424 * DeleteFamily masks all columns, so it must be tracked by the delete tracker. 425 */ 426 @Test 427 public void testDeleteColumnEmptyQualifierDoesNotSkipDeleteFamily() throws IOException { 428 long now = EnvironmentEdgeManager.currentTime(); 429 byte[] e = HConstants.EMPTY_BYTE_ARRAY; 430 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1, 431 ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 432 now - ttl, now, null); 433 434 int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD; 435 // Feed DCs with empty qualifier past the threshold, then a DF. 436 // The DF must NOT be seeked past -- it must be SKIP'd so the tracker picks it up. 437 qm.setToNewRow(new KeyValue(row1, fam1, e, now, Type.DeleteColumn)); 438 for (int i = 0; i < n + 1; i++) { 439 // Empty qualifier DCs should never trigger seek, regardless of threshold 440 assertEquals("DC at i=" + i, MatchCode.SKIP, 441 qm.match(new KeyValue(row1, fam1, e, now - i, Type.DeleteColumn))); 442 } 443 KeyValue df = new KeyValue(row1, fam1, e, now - n - 1, Type.DeleteFamily); 444 KeyValue put = new KeyValue(row1, fam1, col1, now - n - 1, Type.Put, data); 445 // DF must be processed (SKIP), not seeked past 446 assertEquals(MatchCode.SKIP, qm.match(df)); 447 // Put in col1 at t=now-3 should be masked by DF@t=now-3 448 MatchCode putCode = qm.match(put); 449 assertEquals(MatchCode.SEEK_NEXT_COL, putCode); 450 } 451 452 /** 453 * DeleteColumn markers for different qualifiers should not accumulate the seek counter. Only 454 * consecutive markers for the same qualifier should trigger seeking. 455 */ 456 @Test 457 public void testDeleteColumnDifferentQualifiersDoNotSeek() throws IOException { 458 long now = EnvironmentEdgeManager.currentTime(); 459 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1, 460 ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, 461 now - ttl, now, null); 462 463 // DCs for different qualifiers: counter resets on qualifier change, never seeks 464 qm.setToNewRow(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn)); 465 assertEquals(MatchCode.SKIP, qm.match(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn))); 466 assertEquals(MatchCode.SKIP, 467 qm.match(new KeyValue(row1, fam1, col2, now - 1, Type.DeleteColumn))); 468 assertEquals(MatchCode.SKIP, 469 qm.match(new KeyValue(row1, fam1, col3, now - 2, Type.DeleteColumn))); 470 assertEquals(MatchCode.SKIP, 471 qm.match(new KeyValue(row1, fam1, col4, now - 3, Type.DeleteColumn))); 472 assertEquals(MatchCode.SKIP, 473 qm.match(new KeyValue(row1, fam1, col5, now - 4, Type.DeleteColumn))); 474 } 475 476 /** 477 * Delete markers outside the scan's time range (includeDeleteMarker=false) should still 478 * accumulate the seek counter and trigger SEEK_NEXT_COL after the threshold. 479 */ 480 @Test 481 public void testSeekOnRangeDeleteOutsideTimeRange() throws IOException { 482 long now = EnvironmentEdgeManager.currentTime(); 483 long futureTs = now + 1_000_000; 484 Scan scanWithTimeRange = new Scan(scan).setTimeRange(futureTs, Long.MAX_VALUE); 485 486 UserScanQueryMatcher qm = UserScanQueryMatcher.create(scanWithTimeRange, 487 new ScanInfo(this.conf, fam1, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 488 0, rowComparator, false), 489 null, now - ttl, now, null); 490 491 int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD; 492 qm.setToNewRow(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn)); 493 // All DCs have timestamps below the time range, so includeDeleteMarker is false. 494 // The seek counter should still accumulate. 495 for (int i = 0; i < n - 1; i++) { 496 assertEquals("DC at i=" + i, MatchCode.SKIP, 497 qm.match(new KeyValue(row1, fam1, col1, now - i, Type.DeleteColumn))); 498 } 499 assertEquals(MatchCode.SEEK_NEXT_COL, 500 qm.match(new KeyValue(row1, fam1, col1, now - n + 1, Type.DeleteColumn))); 501 } 502 503 private UserScanQueryMatcher createDeleteMatcher(KeepDeletedCells keepDeletedCells) 504 throws IOException { 505 long now = EnvironmentEdgeManager.currentTime(); 506 return UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1, ttl, 507 keepDeletedCells, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, now - ttl, 508 now, null); 509 } 510 511 /** First n-1 markers SKIP, n-th triggers SEEK_NEXT_COL. */ 512 private void assertSeekAfterThreshold(KeepDeletedCells keepDeletedCells, Type type, int n) 513 throws IOException { 514 long now = EnvironmentEdgeManager.currentTime(); 515 UserScanQueryMatcher qm = createDeleteMatcher(keepDeletedCells); 516 boolean familyLevel = type == Type.DeleteFamily || type == Type.DeleteFamilyVersion; 517 byte[] qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1; 518 qm.setToNewRow(new KeyValue(row1, fam1, qual, now, type)); 519 for (int i = 0; i < n - 1; i++) { 520 assertEquals("Mismatch at index " + i, MatchCode.SKIP, 521 qm.match(new KeyValue(row1, fam1, qual, now - i, type))); 522 } 523 assertEquals("Expected SEEK_NEXT_COL at index " + (n - 1), MatchCode.SEEK_NEXT_COL, 524 qm.match(new KeyValue(row1, fam1, qual, now - n + 1, type))); 525 } 526 527 /** All markers should SKIP regardless of count. */ 528 private void assertAllSkip(KeepDeletedCells keepDeletedCells, Type type, int count) 529 throws IOException { 530 long now = EnvironmentEdgeManager.currentTime(); 531 UserScanQueryMatcher qm = createDeleteMatcher(keepDeletedCells); 532 boolean familyLevel = type == Type.DeleteFamily || type == Type.DeleteFamilyVersion; 533 byte[] qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1; 534 qm.setToNewRow(new KeyValue(row1, fam1, qual, now, type)); 535 for (int i = 0; i < count; i++) { 536 assertEquals("Mismatch at index " + i, MatchCode.SKIP, 537 qm.match(new KeyValue(row1, fam1, qual, now - i, type))); 538 } 539 } 540}