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;
019
020import static org.apache.hadoop.hbase.HBaseTestingUtil.COLUMNS;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.List;
027import org.apache.hadoop.hbase.Cell;
028import org.apache.hadoop.hbase.CellUtil;
029import org.apache.hadoop.hbase.HBaseTestingUtil;
030import org.apache.hadoop.hbase.KeepDeletedCells;
031import org.apache.hadoop.hbase.TableName;
032import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
033import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
034import org.apache.hadoop.hbase.client.Delete;
035import org.apache.hadoop.hbase.client.Get;
036import org.apache.hadoop.hbase.client.Put;
037import org.apache.hadoop.hbase.client.Result;
038import org.apache.hadoop.hbase.client.TableDescriptor;
039import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
040import org.apache.hadoop.hbase.filter.TimestampsFilter;
041import org.apache.hadoop.hbase.testclassification.MediumTests;
042import org.apache.hadoop.hbase.testclassification.RegionServerTests;
043import org.apache.hadoop.hbase.util.Bytes;
044import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
045import org.apache.hadoop.hbase.util.ManualEnvironmentEdge;
046import org.junit.jupiter.api.BeforeEach;
047import org.junit.jupiter.api.Tag;
048import org.junit.jupiter.api.Test;
049import org.junit.jupiter.api.TestInfo;
050
051/**
052 * Test Minimum Versions feature (HBASE-4071).
053 */
054@Tag(RegionServerTests.TAG)
055@Tag(MediumTests.TAG)
056public class TestMinVersions {
057
058  HBaseTestingUtil hbu = new HBaseTestingUtil();
059  private final byte[] T0 = Bytes.toBytes("0");
060  private final byte[] T1 = Bytes.toBytes("1");
061  private final byte[] T2 = Bytes.toBytes("2");
062  private final byte[] T3 = Bytes.toBytes("3");
063  private final byte[] T4 = Bytes.toBytes("4");
064  private final byte[] T5 = Bytes.toBytes("5");
065
066  private final byte[] c0 = COLUMNS[0];
067  private String name;
068
069  /**
070   * Verify behavior of getClosestBefore(...)
071   */
072  @BeforeEach
073  public void setTestName(TestInfo testInfo) {
074    this.name = testInfo.getTestMethod().get().getName();
075  }
076
077  @Test
078  public void testGetClosestBefore() throws Exception {
079
080    ColumnFamilyDescriptor cfd = ColumnFamilyDescriptorBuilder.newBuilder(c0).setMinVersions(1)
081      .setMaxVersions(1000).setTimeToLive(1).setKeepDeletedCells(KeepDeletedCells.FALSE).build();
082
083    TableDescriptor htd =
084      TableDescriptorBuilder.newBuilder(TableName.valueOf(name)).setColumnFamily(cfd).build();
085    HRegion region = hbu.createLocalHRegion(htd, null, null);
086    try {
087
088      // 2s in the past
089      long ts = EnvironmentEdgeManager.currentTime() - 2000;
090
091      Put p = new Put(T1, ts);
092      p.addColumn(c0, c0, T1);
093      region.put(p);
094
095      p = new Put(T1, ts + 1);
096      p.addColumn(c0, c0, T4);
097      region.put(p);
098
099      p = new Put(T3, ts);
100      p.addColumn(c0, c0, T3);
101      region.put(p);
102
103      // now make sure that getClosestBefore(...) get can
104      // rows that would be expired without minVersion.
105      // also make sure it gets the latest version
106      Result r = hbu.getClosestRowBefore(region, T1, c0);
107      checkResult(r, c0, T4);
108
109      r = hbu.getClosestRowBefore(region, T2, c0);
110      checkResult(r, c0, T4);
111
112      // now flush/compact
113      region.flush(true);
114      region.compact(true);
115
116      r = hbu.getClosestRowBefore(region, T1, c0);
117      checkResult(r, c0, T4);
118
119      r = hbu.getClosestRowBefore(region, T2, c0);
120      checkResult(r, c0, T4);
121    } finally {
122      HBaseTestingUtil.closeRegionAndWAL(region);
123    }
124  }
125
126  /**
127   * Test mixed memstore and storefile scanning with minimum versions.
128   */
129  @Test
130  public void testStoreMemStore() throws Exception {
131    // keep 3 versions minimum
132
133    ColumnFamilyDescriptor cfd = ColumnFamilyDescriptorBuilder.newBuilder(c0).setMinVersions(3)
134      .setMaxVersions(1000).setTimeToLive(1).setKeepDeletedCells(KeepDeletedCells.FALSE).build();
135
136    TableDescriptor htd =
137      TableDescriptorBuilder.newBuilder(TableName.valueOf(name)).setColumnFamily(cfd).build();
138
139    HRegion region = hbu.createLocalHRegion(htd, null, null);
140    // 2s in the past
141    long ts = EnvironmentEdgeManager.currentTime() - 2000;
142
143    try {
144      Put p = new Put(T1, ts - 1);
145      p.addColumn(c0, c0, T2);
146      region.put(p);
147
148      p = new Put(T1, ts - 3);
149      p.addColumn(c0, c0, T0);
150      region.put(p);
151
152      // now flush/compact
153      region.flush(true);
154      region.compact(true);
155
156      p = new Put(T1, ts);
157      p.addColumn(c0, c0, T3);
158      region.put(p);
159
160      p = new Put(T1, ts - 2);
161      p.addColumn(c0, c0, T1);
162      region.put(p);
163
164      p = new Put(T1, ts - 3);
165      p.addColumn(c0, c0, T0);
166      region.put(p);
167
168      // newest version in the memstore
169      // the 2nd oldest in the store file
170      // and the 3rd, 4th oldest also in the memstore
171
172      Get g = new Get(T1);
173      g.readAllVersions();
174      Result r = region.get(g); // this'll use ScanWildcardColumnTracker
175      checkResult(r, c0, T3, T2, T1);
176
177      g = new Get(T1);
178      g.readAllVersions();
179      g.addColumn(c0, c0);
180      r = region.get(g); // this'll use ExplicitColumnTracker
181      checkResult(r, c0, T3, T2, T1);
182    } finally {
183      HBaseTestingUtil.closeRegionAndWAL(region);
184    }
185  }
186
187  /**
188   * Make sure the Deletes behave as expected with minimum versions
189   */
190  @Test
191  public void testDelete() throws Exception {
192    ColumnFamilyDescriptor cfd = ColumnFamilyDescriptorBuilder.newBuilder(c0).setMinVersions(3)
193      .setMaxVersions(1000).setTimeToLive(1).setKeepDeletedCells(KeepDeletedCells.FALSE).build();
194
195    TableDescriptor htd =
196      TableDescriptorBuilder.newBuilder(TableName.valueOf(name)).setColumnFamily(cfd).build();
197
198    HRegion region = hbu.createLocalHRegion(htd, null, null);
199
200    // 2s in the past
201    long ts = EnvironmentEdgeManager.currentTime() - 2000;
202
203    try {
204      Put p = new Put(T1, ts - 2);
205      p.addColumn(c0, c0, T1);
206      region.put(p);
207
208      p = new Put(T1, ts - 1);
209      p.addColumn(c0, c0, T2);
210      region.put(p);
211
212      p = new Put(T1, ts);
213      p.addColumn(c0, c0, T3);
214      region.put(p);
215
216      Delete d = new Delete(T1, ts - 1);
217      region.delete(d);
218
219      Get g = new Get(T1);
220      g.readAllVersions();
221      Result r = region.get(g); // this'll use ScanWildcardColumnTracker
222      checkResult(r, c0, T3);
223
224      g = new Get(T1);
225      g.readAllVersions();
226      g.addColumn(c0, c0);
227      r = region.get(g); // this'll use ExplicitColumnTracker
228      checkResult(r, c0, T3);
229
230      // now flush/compact
231      region.flush(true);
232      region.compact(true);
233
234      // try again
235      g = new Get(T1);
236      g.readAllVersions();
237      r = region.get(g); // this'll use ScanWildcardColumnTracker
238      checkResult(r, c0, T3);
239
240      g = new Get(T1);
241      g.readAllVersions();
242      g.addColumn(c0, c0);
243      r = region.get(g); // this'll use ExplicitColumnTracker
244      checkResult(r, c0, T3);
245    } finally {
246      HBaseTestingUtil.closeRegionAndWAL(region);
247    }
248  }
249
250  /**
251   * Make sure the memstor behaves correctly with minimum versions
252   */
253  @Test
254  public void testMemStore() throws Exception {
255    ColumnFamilyDescriptor cfd = ColumnFamilyDescriptorBuilder.newBuilder(c0).setMinVersions(2)
256      .setMaxVersions(1000).setTimeToLive(1).setKeepDeletedCells(KeepDeletedCells.FALSE).build();
257
258    TableDescriptor htd =
259      TableDescriptorBuilder.newBuilder(TableName.valueOf(name)).setColumnFamily(cfd).build();
260    HRegion region = hbu.createLocalHRegion(htd, null, null);
261
262    // 2s in the past
263    long ts = EnvironmentEdgeManager.currentTime() - 2000;
264
265    try {
266      // 2nd version
267      Put p = new Put(T1, ts - 2);
268      p.addColumn(c0, c0, T2);
269      region.put(p);
270
271      // 3rd version
272      p = new Put(T1, ts - 1);
273      p.addColumn(c0, c0, T3);
274      region.put(p);
275
276      // 4th version
277      p = new Put(T1, ts);
278      p.addColumn(c0, c0, T4);
279      region.put(p);
280
281      // now flush/compact
282      region.flush(true);
283      region.compact(true);
284
285      // now put the first version (backdated)
286      p = new Put(T1, ts - 3);
287      p.addColumn(c0, c0, T1);
288      region.put(p);
289
290      // now the latest change is in the memstore,
291      // but it is not the latest version
292
293      Result r = region.get(new Get(T1));
294      checkResult(r, c0, T4);
295
296      Get g = new Get(T1);
297      g.readAllVersions();
298      r = region.get(g); // this'll use ScanWildcardColumnTracker
299      checkResult(r, c0, T4, T3);
300
301      g = new Get(T1);
302      g.readAllVersions();
303      g.addColumn(c0, c0);
304      r = region.get(g); // this'll use ExplicitColumnTracker
305      checkResult(r, c0, T4, T3);
306
307      p = new Put(T1, ts + 1);
308      p.addColumn(c0, c0, T5);
309      region.put(p);
310
311      // now the latest version is in the memstore
312
313      g = new Get(T1);
314      g.readAllVersions();
315      r = region.get(g); // this'll use ScanWildcardColumnTracker
316      checkResult(r, c0, T5, T4);
317
318      g = new Get(T1);
319      g.readAllVersions();
320      g.addColumn(c0, c0);
321      r = region.get(g); // this'll use ExplicitColumnTracker
322      checkResult(r, c0, T5, T4);
323    } finally {
324      HBaseTestingUtil.closeRegionAndWAL(region);
325    }
326  }
327
328  /**
329   * Verify basic minimum versions functionality
330   */
331  @Test
332  public void testBaseCase() throws Exception {
333    // 2 version minimum, 1000 versions maximum, ttl = 1s
334    ColumnFamilyDescriptor cfd = ColumnFamilyDescriptorBuilder.newBuilder(c0).setMinVersions(2)
335      .setMaxVersions(1000).setTimeToLive(1).setKeepDeletedCells(KeepDeletedCells.FALSE).build();
336
337    TableDescriptor htd =
338      TableDescriptorBuilder.newBuilder(TableName.valueOf(name)).setColumnFamily(cfd).build();
339    HRegion region = hbu.createLocalHRegion(htd, null, null);
340    try {
341
342      // 2s in the past
343      long ts = EnvironmentEdgeManager.currentTime() - 2000;
344
345      // 1st version
346      Put p = new Put(T1, ts - 3);
347      p.addColumn(c0, c0, T1);
348      region.put(p);
349
350      // 2nd version
351      p = new Put(T1, ts - 2);
352      p.addColumn(c0, c0, T2);
353      region.put(p);
354
355      // 3rd version
356      p = new Put(T1, ts - 1);
357      p.addColumn(c0, c0, T3);
358      region.put(p);
359
360      // 4th version
361      p = new Put(T1, ts);
362      p.addColumn(c0, c0, T4);
363      region.put(p);
364
365      Result r = region.get(new Get(T1));
366      checkResult(r, c0, T4);
367
368      Get g = new Get(T1);
369      g.setTimeRange(0L, ts + 1);
370      r = region.get(g);
371      checkResult(r, c0, T4);
372
373      // oldest version still exists
374      g.setTimeRange(0L, ts - 2);
375      r = region.get(g);
376      checkResult(r, c0, T1);
377
378      // gets see only available versions
379      // even before compactions
380      g = new Get(T1);
381      g.readAllVersions();
382      r = region.get(g); // this'll use ScanWildcardColumnTracker
383      checkResult(r, c0, T4, T3);
384
385      g = new Get(T1);
386      g.readAllVersions();
387      g.addColumn(c0, c0);
388      r = region.get(g); // this'll use ExplicitColumnTracker
389      checkResult(r, c0, T4, T3);
390
391      // now flush
392      region.flush(true);
393
394      // with HBASE-4241 a flush will eliminate the expired rows
395      g = new Get(T1);
396      g.setTimeRange(0L, ts - 2);
397      r = region.get(g);
398      assertTrue(r.isEmpty());
399
400      // major compaction
401      region.compact(true);
402
403      // after compaction the 4th version is still available
404      g = new Get(T1);
405      g.setTimeRange(0L, ts + 1);
406      r = region.get(g);
407      checkResult(r, c0, T4);
408
409      // so is the 3rd
410      g.setTimeRange(0L, ts);
411      r = region.get(g);
412      checkResult(r, c0, T3);
413
414      // but the 2nd and earlier versions are gone
415      g.setTimeRange(0L, ts - 1);
416      r = region.get(g);
417      assertTrue(r.isEmpty());
418    } finally {
419      HBaseTestingUtil.closeRegionAndWAL(region);
420    }
421  }
422
423  /**
424   * Verify that basic filters still behave correctly with minimum versions enabled.
425   */
426  @Test
427  public void testFilters() throws Exception {
428    final byte[] c1 = COLUMNS[1];
429    ColumnFamilyDescriptor cfd = ColumnFamilyDescriptorBuilder.newBuilder(c0).setMinVersions(2)
430      .setMaxVersions(1000).setTimeToLive(1).setKeepDeletedCells(KeepDeletedCells.FALSE).build();
431
432    ColumnFamilyDescriptor cfd2 = ColumnFamilyDescriptorBuilder.newBuilder(c1).setMinVersions(2)
433      .setMaxVersions(1000).setTimeToLive(1).setKeepDeletedCells(KeepDeletedCells.FALSE).build();
434    List<ColumnFamilyDescriptor> cfdList = new ArrayList();
435    cfdList.add(cfd);
436    cfdList.add(cfd2);
437
438    TableDescriptor htd =
439      TableDescriptorBuilder.newBuilder(TableName.valueOf(name)).setColumnFamilies(cfdList).build();
440    HRegion region = hbu.createLocalHRegion(htd, null, null);
441
442    // 2s in the past
443    long ts = EnvironmentEdgeManager.currentTime() - 2000;
444    try {
445
446      Put p = new Put(T1, ts - 3);
447      p.addColumn(c0, c0, T0);
448      p.addColumn(c1, c1, T0);
449      region.put(p);
450
451      p = new Put(T1, ts - 2);
452      p.addColumn(c0, c0, T1);
453      p.addColumn(c1, c1, T1);
454      region.put(p);
455
456      p = new Put(T1, ts - 1);
457      p.addColumn(c0, c0, T2);
458      p.addColumn(c1, c1, T2);
459      region.put(p);
460
461      p = new Put(T1, ts);
462      p.addColumn(c0, c0, T3);
463      p.addColumn(c1, c1, T3);
464      region.put(p);
465
466      List<Long> tss = new ArrayList<>();
467      tss.add(ts - 1);
468      tss.add(ts - 2);
469
470      // Sholud only get T2, versions is 2, so T1 is gone from user view.
471      Get g = new Get(T1);
472      g.addColumn(c1, c1);
473      g.setFilter(new TimestampsFilter(tss));
474      g.readAllVersions();
475      Result r = region.get(g);
476      checkResult(r, c1, T2);
477
478      // Sholud only get T2, versions is 2, so T1 is gone from user view.
479      g = new Get(T1);
480      g.addColumn(c0, c0);
481      g.setFilter(new TimestampsFilter(tss));
482      g.readAllVersions();
483      r = region.get(g);
484      checkResult(r, c0, T2);
485
486      // now flush/compact
487      region.flush(true);
488      region.compact(true);
489
490      // After flush/compact, the result should be consistent with previous result
491      g = new Get(T1);
492      g.addColumn(c1, c1);
493      g.setFilter(new TimestampsFilter(tss));
494      g.readAllVersions();
495      r = region.get(g);
496      checkResult(r, c1, T2);
497
498      // After flush/compact, the result should be consistent with previous result
499      g = new Get(T1);
500      g.addColumn(c0, c0);
501      g.setFilter(new TimestampsFilter(tss));
502      g.readAllVersions();
503      r = region.get(g);
504      checkResult(r, c0, T2);
505    } finally {
506      HBaseTestingUtil.closeRegionAndWAL(region);
507    }
508  }
509
510  @Test
511  public void testMinVersionsWithKeepDeletedCellsTTL() throws Exception {
512    int ttl = 4;
513    ColumnFamilyDescriptor cfd =
514      ColumnFamilyDescriptorBuilder.newBuilder(c0).setVersionsWithTimeToLive(ttl, 2).build();
515    verifyVersionedCellKeyValues(ttl, cfd);
516
517    cfd = ColumnFamilyDescriptorBuilder.newBuilder(c0).setMinVersions(2)
518      .setMaxVersions(Integer.MAX_VALUE).setTimeToLive(ttl)
519      .setKeepDeletedCells(KeepDeletedCells.TTL).build();
520    verifyVersionedCellKeyValues(ttl, cfd);
521  }
522
523  private void verifyVersionedCellKeyValues(int ttl, ColumnFamilyDescriptor cfd)
524    throws IOException {
525    TableDescriptor htd =
526      TableDescriptorBuilder.newBuilder(TableName.valueOf(name)).setColumnFamily(cfd).build();
527
528    HRegion region = hbu.createLocalHRegion(htd, null, null);
529
530    try {
531      long startTS = EnvironmentEdgeManager.currentTime();
532      ManualEnvironmentEdge injectEdge = new ManualEnvironmentEdge();
533      injectEdge.setValue(startTS);
534      EnvironmentEdgeManager.injectEdge(injectEdge);
535
536      long ts = startTS - 2000;
537      putFourVersions(region, ts);
538
539      Get get;
540      Result result;
541
542      // check we can still see all versions before compaction
543      get = new Get(T1);
544      get.readAllVersions();
545      get.setTimeRange(0, ts);
546      result = region.get(get);
547      checkResult(result, c0, T4, T3, T2, T1);
548
549      region.flush(true);
550      region.compact(true);
551      assertEquals(startTS, EnvironmentEdgeManager.currentTime());
552      long expiredTime = EnvironmentEdgeManager.currentTime() - ts - 4;
553      assertTrue(expiredTime < (ttl * 1000), "TTL for T1 has expired");
554      // check that nothing was purged yet
555      verifyBeforeCompaction(region, ts);
556
557      injectEdge.incValue(ttl * 1000);
558
559      region.flush(true);
560      region.compact(true);
561      verifyAfterTtl(region, ts);
562    } finally {
563      HBaseTestingUtil.closeRegionAndWAL(region);
564    }
565  }
566
567  private void verifyAfterTtl(HRegion region, long ts) throws IOException {
568    Get get;
569    Result result;
570    // check that after compaction (which is after TTL) that only T1 && T2 were purged
571    get = new Get(T1);
572    get.readAllVersions();
573    get.setTimeRange(0, ts);
574    result = region.get(get);
575    checkResult(result, c0, T4, T3);
576
577    get = new Get(T1);
578    get.readAllVersions();
579    get.setTimeRange(0, ts - 1);
580    result = region.get(get);
581    checkResult(result, c0, T3);
582
583    get = new Get(T1);
584    get.readAllVersions();
585    get.setTimestamp(ts - 2);
586    result = region.get(get);
587    checkResult(result, c0, T3);
588
589    get = new Get(T1);
590    get.readAllVersions();
591    get.setTimestamp(ts - 3);
592    result = region.get(get);
593    assertEquals(result.getColumnCells(c0, c0).size(), 0);
594
595    get = new Get(T1);
596    get.readAllVersions();
597    get.setTimeRange(0, ts - 2);
598    result = region.get(get);
599    assertEquals(result.getColumnCells(c0, c0).size(), 0);
600  }
601
602  private void verifyBeforeCompaction(HRegion region, long ts) throws IOException {
603    Get get;
604    Result result;
605    get = new Get(T1);
606    get.readAllVersions();
607    get.setTimeRange(0, ts);
608    result = region.get(get);
609    checkResult(result, c0, T4, T3, T2, T1);
610
611    get = new Get(T1);
612    get.readAllVersions();
613    get.setTimeRange(0, ts - 1);
614    result = region.get(get);
615    checkResult(result, c0, T3, T2, T1);
616
617    get = new Get(T1);
618    get.readAllVersions();
619    get.setTimeRange(0, ts - 2);
620    result = region.get(get);
621    checkResult(result, c0, T2, T1);
622
623    get = new Get(T1);
624    get.readAllVersions();
625    get.setTimeRange(0, ts - 3);
626    result = region.get(get);
627    checkResult(result, c0, T1);
628  }
629
630  private void putFourVersions(HRegion region, long ts) throws IOException {
631    // 1st version
632    Put put = new Put(T1, ts - 4);
633    put.addColumn(c0, c0, T1);
634    region.put(put);
635
636    // 2nd version
637    put = new Put(T1, ts - 3);
638    put.addColumn(c0, c0, T2);
639    region.put(put);
640
641    // 3rd version
642    put = new Put(T1, ts - 2);
643    put.addColumn(c0, c0, T3);
644    region.put(put);
645
646    // 4th version
647    put = new Put(T1, ts - 1);
648    put.addColumn(c0, c0, T4);
649    region.put(put);
650  }
651
652  private void checkResult(Result r, byte[] col, byte[]... vals) {
653    assertEquals(vals.length, r.size());
654    List<Cell> kvs = r.getColumnCells(col, col);
655    assertEquals(kvs.size(), vals.length);
656    for (int i = 0; i < vals.length; i++) {
657      String expected = Bytes.toString(vals[i]);
658      String actual = Bytes.toString(CellUtil.cloneValue(kvs.get(i)));
659      assertTrue(CellUtil.matchingValue(kvs.get(i), vals[i]),
660        expected + " was expected but doesn't match " + actual);
661    }
662  }
663
664}