001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.hadoop.hbase.regionserver;
020
021import org.apache.hadoop.hbase.exceptions.IllegalArgumentIOException;
022import org.apache.hadoop.util.StringUtils;
023import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.concurrent.ThreadPoolExecutor;
028import java.util.concurrent.atomic.AtomicBoolean;
029
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.hbase.Cell;
032import org.apache.hadoop.hbase.CellComparator;
033import org.apache.hadoop.hbase.HConstants;
034import org.apache.hadoop.hbase.MemoryCompactionPolicy;
035import org.apache.yetus.audience.InterfaceAudience;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038import org.apache.hadoop.hbase.util.Bytes;
039import org.apache.hadoop.hbase.util.ClassSize;
040import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
041import org.apache.hadoop.hbase.wal.WAL;
042
043/**
044 * A memstore implementation which supports in-memory compaction.
045 * A compaction pipeline is added between the active set and the snapshot data structures;
046 * it consists of a list of segments that are subject to compaction.
047 * Like the snapshot, all pipeline segments are read-only; updates only affect the active set.
048 * To ensure this property we take advantage of the existing blocking mechanism -- the active set
049 * is pushed to the pipeline while holding the region's updatesLock in exclusive mode.
050 * Periodically, a compaction is applied in the background to all pipeline segments resulting
051 * in a single read-only component. The ``old'' segments are discarded when no scanner is reading
052 * them.
053 */
054@InterfaceAudience.Private
055public class CompactingMemStore extends AbstractMemStore {
056
057  // The external setting of the compacting MemStore behaviour
058  public static final String COMPACTING_MEMSTORE_TYPE_KEY =
059      "hbase.hregion.compacting.memstore.type";
060  public static final String COMPACTING_MEMSTORE_TYPE_DEFAULT =
061      String.valueOf(MemoryCompactionPolicy.NONE);
062  // Default fraction of in-memory-flush size w.r.t. flush-to-disk size
063  public static final String IN_MEMORY_FLUSH_THRESHOLD_FACTOR_KEY =
064      "hbase.memstore.inmemoryflush.threshold.factor";
065  private static final double IN_MEMORY_FLUSH_THRESHOLD_FACTOR_DEFAULT = 0.014;
066
067  private static final Logger LOG = LoggerFactory.getLogger(CompactingMemStore.class);
068  private HStore store;
069  private CompactionPipeline pipeline;
070  protected MemStoreCompactor compactor;
071
072  private long inmemoryFlushSize;       // the threshold on active size for in-memory flush
073  private final AtomicBoolean inMemoryFlushInProgress = new AtomicBoolean(false);
074
075  // inWalReplay is true while we are synchronously replaying the edits from WAL
076  private boolean inWalReplay = false;
077
078  @VisibleForTesting
079  protected final AtomicBoolean allowCompaction = new AtomicBoolean(true);
080  private boolean compositeSnapshot = true;
081
082  /**
083   * Types of indexes (part of immutable segments) to be used after flattening,
084   * compaction, or merge are applied.
085   */
086  public enum IndexType {
087    CSLM_MAP,   // ConcurrentSkipLisMap
088    ARRAY_MAP,  // CellArrayMap
089    CHUNK_MAP   // CellChunkMap
090  }
091
092  private IndexType indexType = IndexType.ARRAY_MAP;  // default implementation
093
094  public static final long DEEP_OVERHEAD = ClassSize.align( AbstractMemStore.DEEP_OVERHEAD
095      + 6 * ClassSize.REFERENCE     // Store, CompactionPipeline,
096                                    // MemStoreCompactor, inMemoryFlushInProgress, allowCompaction,
097                                    // indexType
098      + Bytes.SIZEOF_LONG           // inmemoryFlushSize
099      + 2 * Bytes.SIZEOF_BOOLEAN    // compositeSnapshot and inWalReplay
100      + 2 * ClassSize.ATOMIC_BOOLEAN// inMemoryFlushInProgress and allowCompaction
101      + CompactionPipeline.DEEP_OVERHEAD + MemStoreCompactor.DEEP_OVERHEAD);
102
103  public CompactingMemStore(Configuration conf, CellComparator c,
104      HStore store, RegionServicesForStores regionServices,
105      MemoryCompactionPolicy compactionPolicy) throws IOException {
106    super(conf, c, regionServices);
107    this.store = store;
108    this.regionServices = regionServices;
109    this.pipeline = new CompactionPipeline(getRegionServices());
110    this.compactor = createMemStoreCompactor(compactionPolicy);
111    if (conf.getBoolean(MemStoreLAB.USEMSLAB_KEY, MemStoreLAB.USEMSLAB_DEFAULT)) {
112      // if user requested to work with MSLABs (whether on- or off-heap), then the
113      // immutable segments are going to use CellChunkMap as their index
114      indexType = IndexType.CHUNK_MAP;
115    } else {
116      indexType = IndexType.ARRAY_MAP;
117    }
118    // initialization of the flush size should happen after initialization of the index type
119    // so do not transfer the following method
120    initInmemoryFlushSize(conf);
121    LOG.info("Store={}, in-memory flush size threshold={}, immutable segments index type={}, " +
122            "compactor={}", this.store.getColumnFamilyName(),
123        StringUtils.byteDesc(this.inmemoryFlushSize), this.indexType,
124        (this.compactor == null? "NULL": this.compactor.toString()));
125  }
126
127  @VisibleForTesting
128  protected MemStoreCompactor createMemStoreCompactor(MemoryCompactionPolicy compactionPolicy)
129      throws IllegalArgumentIOException {
130    return new MemStoreCompactor(this, compactionPolicy);
131  }
132
133  private void initInmemoryFlushSize(Configuration conf) {
134    double factor = 0;
135    long memstoreFlushSize = getRegionServices().getMemStoreFlushSize();
136    int numStores = getRegionServices().getNumStores();
137    if (numStores <= 1) {
138      // Family number might also be zero in some of our unit test case
139      numStores = 1;
140    }
141    inmemoryFlushSize = memstoreFlushSize / numStores;
142    // multiply by a factor (the same factor for all index types)
143    factor = conf.getDouble(IN_MEMORY_FLUSH_THRESHOLD_FACTOR_KEY,
144          IN_MEMORY_FLUSH_THRESHOLD_FACTOR_DEFAULT);
145
146    inmemoryFlushSize = (long) (inmemoryFlushSize * factor);
147  }
148
149  /**
150   * @return Total memory occupied by this MemStore. This won't include any size occupied by the
151   *         snapshot. We assume the snapshot will get cleared soon. This is not thread safe and
152   *         the memstore may be changed while computing its size. It is the responsibility of the
153   *         caller to make sure this doesn't happen.
154   */
155  @Override
156  public MemStoreSize size() {
157    MemStoreSizing memstoreSizing = new NonThreadSafeMemStoreSizing();
158    memstoreSizing.incMemStoreSize(active.getMemStoreSize());
159    for (Segment item : pipeline.getSegments()) {
160      memstoreSizing.incMemStoreSize(item.getMemStoreSize());
161    }
162    return memstoreSizing.getMemStoreSize();
163  }
164
165  /**
166   * This method is called before the flush is executed.
167   * @return an estimation (lower bound) of the unflushed sequence id in memstore after the flush
168   * is executed. if memstore will be cleared returns {@code HConstants.NO_SEQNUM}.
169   */
170  @Override
171  public long preFlushSeqIDEstimation() {
172    if(compositeSnapshot) {
173      return HConstants.NO_SEQNUM;
174    }
175    Segment segment = getLastSegment();
176    if(segment == null) {
177      return HConstants.NO_SEQNUM;
178    }
179    return segment.getMinSequenceId();
180  }
181
182  @Override
183  public boolean isSloppy() {
184    return true;
185  }
186
187  /**
188   * Push the current active memstore segment into the pipeline
189   * and create a snapshot of the tail of current compaction pipeline
190   * Snapshot must be cleared by call to {@link #clearSnapshot}.
191   * {@link #clearSnapshot(long)}.
192   * @return {@link MemStoreSnapshot}
193   */
194  @Override
195  public MemStoreSnapshot snapshot() {
196    // If snapshot currently has entries, then flusher failed or didn't call
197    // cleanup.  Log a warning.
198    if (!this.snapshot.isEmpty()) {
199      LOG.warn("Snapshot called again without clearing previous. " +
200          "Doing nothing. Another ongoing flush or did we fail last attempt?");
201    } else {
202      LOG.debug("FLUSHING TO DISK {}, store={}",
203            getRegionServices().getRegionInfo().getEncodedName(), getFamilyName());
204      stopCompaction();
205      pushActiveToPipeline(this.active);
206      snapshotId = EnvironmentEdgeManager.currentTime();
207      // in both cases whatever is pushed to snapshot is cleared from the pipeline
208      if (compositeSnapshot) {
209        pushPipelineToSnapshot();
210      } else {
211        pushTailToSnapshot();
212      }
213      compactor.resetStats();
214    }
215    return new MemStoreSnapshot(snapshotId, this.snapshot);
216  }
217
218  @Override
219  public MemStoreSize getFlushableSize() {
220    MemStoreSize mss = getSnapshotSize();
221    if (mss.getDataSize() == 0) {
222      // if snapshot is empty the tail of the pipeline (or everything in the memstore) is flushed
223      if (compositeSnapshot) {
224        MemStoreSizing memStoreSizing = new NonThreadSafeMemStoreSizing(pipeline.getPipelineSize());
225        memStoreSizing.incMemStoreSize(this.active.getMemStoreSize());
226        mss = memStoreSizing.getMemStoreSize();
227      } else {
228        mss = pipeline.getTailSize();
229      }
230    }
231    return mss.getDataSize() > 0? mss: this.active.getMemStoreSize();
232  }
233
234  @Override
235  protected long keySize() {
236    // Need to consider dataSize/keySize of all segments in pipeline and active
237    long keySize = this.active.getDataSize();
238    for (Segment segment : this.pipeline.getSegments()) {
239      keySize += segment.getDataSize();
240    }
241    return keySize;
242  }
243
244  @Override
245  protected long heapSize() {
246    // Need to consider heapOverhead of all segments in pipeline and active
247    long h = this.active.getHeapSize();
248    for (Segment segment : this.pipeline.getSegments()) {
249      h += segment.getHeapSize();
250    }
251    return h;
252  }
253
254  @Override
255  public void updateLowestUnflushedSequenceIdInWAL(boolean onlyIfGreater) {
256    long minSequenceId = pipeline.getMinSequenceId();
257    if(minSequenceId != Long.MAX_VALUE) {
258      byte[] encodedRegionName = getRegionServices().getRegionInfo().getEncodedNameAsBytes();
259      byte[] familyName = getFamilyNameInBytes();
260      WAL WAL = getRegionServices().getWAL();
261      if (WAL != null) {
262        WAL.updateStore(encodedRegionName, familyName, minSequenceId, onlyIfGreater);
263      }
264    }
265  }
266
267  /**
268   * This message intends to inform the MemStore that next coming updates
269   * are going to be part of the replaying edits from WAL
270   */
271  @Override
272  public void startReplayingFromWAL() {
273    inWalReplay = true;
274  }
275
276  /**
277   * This message intends to inform the MemStore that the replaying edits from WAL
278   * are done
279   */
280  @Override
281  public void stopReplayingFromWAL() {
282    inWalReplay = false;
283  }
284
285  // the getSegments() method is used for tests only
286  @VisibleForTesting
287  @Override
288  protected List<Segment> getSegments() {
289    List<? extends Segment> pipelineList = pipeline.getSegments();
290    List<Segment> list = new ArrayList<>(pipelineList.size() + 2);
291    list.add(this.active);
292    list.addAll(pipelineList);
293    list.addAll(this.snapshot.getAllSegments());
294
295    return list;
296  }
297
298  // the following three methods allow to manipulate the settings of composite snapshot
299  public void setCompositeSnapshot(boolean useCompositeSnapshot) {
300    this.compositeSnapshot = useCompositeSnapshot;
301  }
302
303  public boolean swapCompactedSegments(VersionedSegmentsList versionedList, ImmutableSegment result,
304      boolean merge) {
305    // last true stands for updating the region size
306    return pipeline.swap(versionedList, result, !merge, true);
307  }
308
309  /**
310   * @param requesterVersion The caller must hold the VersionedList of the pipeline
311   *           with version taken earlier. This version must be passed as a parameter here.
312   *           The flattening happens only if versions match.
313   */
314  public void flattenOneSegment(long requesterVersion,  MemStoreCompactionStrategy.Action action) {
315    pipeline.flattenOneSegment(requesterVersion, indexType, action);
316  }
317
318  // setter is used only for testability
319  @VisibleForTesting
320  void setIndexType(IndexType type) {
321    indexType = type;
322    // Because this functionality is for testing only and tests are setting in-memory flush size
323    // according to their need, there is no setting of in-memory flush size, here.
324    // If it is needed, please change in-memory flush size explicitly
325  }
326
327  public IndexType getIndexType() {
328    return indexType;
329  }
330
331  public boolean hasImmutableSegments() {
332    return !pipeline.isEmpty();
333  }
334
335  public VersionedSegmentsList getImmutableSegments() {
336    return pipeline.getVersionedList();
337  }
338
339  public long getSmallestReadPoint() {
340    return store.getSmallestReadPoint();
341  }
342
343  public HStore getStore() {
344    return store;
345  }
346
347  public String getFamilyName() {
348    return Bytes.toString(getFamilyNameInBytes());
349  }
350
351  @Override
352  /*
353   * Scanners are ordered from 0 (oldest) to newest in increasing order.
354   */
355  public List<KeyValueScanner> getScanners(long readPt) throws IOException {
356    MutableSegment activeTmp = active;
357    List<? extends Segment> pipelineList = pipeline.getSegments();
358    List<? extends Segment> snapshotList = snapshot.getAllSegments();
359    long order = 1L + pipelineList.size() + snapshotList.size();
360    // The list of elements in pipeline + the active element + the snapshot segment
361    // The order is the Segment ordinal
362    List<KeyValueScanner> list = createList((int) order);
363    order = addToScanners(activeTmp, readPt, order, list);
364    order = addToScanners(pipelineList, readPt, order, list);
365    addToScanners(snapshotList, readPt, order, list);
366    return list;
367  }
368
369   @VisibleForTesting
370   protected List<KeyValueScanner> createList(int capacity) {
371     return new ArrayList<>(capacity);
372   }
373
374  /**
375   * Check whether anything need to be done based on the current active set size.
376   * The method is invoked upon every addition to the active set.
377   * For CompactingMemStore, flush the active set to the read-only memory if it's
378   * size is above threshold
379   */
380  @Override
381  protected void checkActiveSize() {
382    if (shouldFlushInMemory()) {
383      /* The thread is dispatched to flush-in-memory. This cannot be done
384      * on the same thread, because for flush-in-memory we require updatesLock
385      * in exclusive mode while this method (checkActiveSize) is invoked holding updatesLock
386      * in the shared mode. */
387      InMemoryFlushRunnable runnable = new InMemoryFlushRunnable();
388      if (LOG.isTraceEnabled()) {
389        LOG.trace(
390          "Dispatching the MemStore in-memory flush for store " + store.getColumnFamilyName());
391      }
392      getPool().execute(runnable);
393    }
394  }
395
396  // internally used method, externally visible only for tests
397  // when invoked directly from tests it must be verified that the caller doesn't hold updatesLock,
398  // otherwise there is a deadlock
399  @VisibleForTesting
400  void flushInMemory() throws IOException {
401    // setting the inMemoryFlushInProgress flag again for the case this method is invoked
402    // directly (only in tests) in the common path setting from true to true is idempotent
403    inMemoryFlushInProgress.set(true);
404    try {
405      // Phase I: Update the pipeline
406      getRegionServices().blockUpdates();
407      try {
408        LOG.trace("IN-MEMORY FLUSH: Pushing active segment into compaction pipeline");
409        pushActiveToPipeline(this.active);
410      } finally {
411        getRegionServices().unblockUpdates();
412      }
413
414      // Used by tests
415      if (!allowCompaction.get()) {
416        return;
417      }
418      // Phase II: Compact the pipeline
419      try {
420        // Speculative compaction execution, may be interrupted if flush is forced while
421        // compaction is in progress
422        compactor.start();
423      } catch (IOException e) {
424        LOG.warn("Unable to run in-memory compaction on {}/{}; exception={}",
425            getRegionServices().getRegionInfo().getEncodedName(), getFamilyName(), e);
426      }
427    } finally {
428      inMemoryFlushInProgress.set(false);
429      LOG.trace("IN-MEMORY FLUSH: end");
430    }
431  }
432
433  private Segment getLastSegment() {
434    Segment localActive = getActive();
435    Segment tail = pipeline.getTail();
436    return tail == null ? localActive : tail;
437  }
438
439  private byte[] getFamilyNameInBytes() {
440    return store.getColumnFamilyDescriptor().getName();
441  }
442
443  private ThreadPoolExecutor getPool() {
444    return getRegionServices().getInMemoryCompactionPool();
445  }
446
447  @VisibleForTesting
448  protected boolean shouldFlushInMemory() {
449    if (this.active.getDataSize() > inmemoryFlushSize) { // size above flush threshold
450      if (inWalReplay) {  // when replaying edits from WAL there is no need in in-memory flush
451        return false;     // regardless the size
452      }
453      // the inMemoryFlushInProgress is CASed to be true here in order to mutual exclude
454      // the insert of the active into the compaction pipeline
455      return (inMemoryFlushInProgress.compareAndSet(false,true));
456    }
457    return false;
458  }
459
460  /**
461   * The request to cancel the compaction asynchronous task (caused by in-memory flush)
462   * The compaction may still happen if the request was sent too late
463   * Non-blocking request
464   */
465  private void stopCompaction() {
466    if (inMemoryFlushInProgress.get()) {
467      compactor.stop();
468    }
469  }
470
471  protected void pushActiveToPipeline(MutableSegment active) {
472    if (!active.isEmpty()) {
473      pipeline.pushHead(active);
474      resetActive();
475    }
476  }
477
478  private void pushTailToSnapshot() {
479    VersionedSegmentsList segments = pipeline.getVersionedTail();
480    pushToSnapshot(segments.getStoreSegments());
481    // In Swap: don't close segments (they are in snapshot now) and don't update the region size
482    pipeline.swap(segments,null,false, false);
483  }
484
485  private void pushPipelineToSnapshot() {
486    int iterationsCnt = 0;
487    boolean done = false;
488    while (!done) {
489      iterationsCnt++;
490      VersionedSegmentsList segments = pipeline.getVersionedList();
491      pushToSnapshot(segments.getStoreSegments());
492      // swap can return false in case the pipeline was updated by ongoing compaction
493      // and the version increase, the chance of it happenning is very low
494      // In Swap: don't close segments (they are in snapshot now) and don't update the region size
495      done = pipeline.swap(segments, null, false, false);
496      if (iterationsCnt>2) {
497        // practically it is impossible that this loop iterates more than two times
498        // (because the compaction is stopped and none restarts it while in snapshot request),
499        // however stopping here for the case of the infinite loop causing by any error
500        LOG.warn("Multiple unsuccessful attempts to push the compaction pipeline to snapshot," +
501            " while flushing to disk.");
502        this.snapshot = SegmentFactory.instance().createImmutableSegment(getComparator());
503        break;
504      }
505    }
506  }
507
508  private void pushToSnapshot(List<ImmutableSegment> segments) {
509    if(segments.isEmpty()) return;
510    if(segments.size() == 1 && !segments.get(0).isEmpty()) {
511      this.snapshot = segments.get(0);
512      return;
513    } else { // create composite snapshot
514      this.snapshot =
515          SegmentFactory.instance().createCompositeImmutableSegment(getComparator(), segments);
516    }
517  }
518
519  private RegionServicesForStores getRegionServices() {
520    return regionServices;
521  }
522
523  /**
524  * The in-memory-flusher thread performs the flush asynchronously.
525  * There is at most one thread per memstore instance.
526  * It takes the updatesLock exclusively, pushes active into the pipeline, releases updatesLock
527  * and compacts the pipeline.
528  */
529  private class InMemoryFlushRunnable implements Runnable {
530
531    @Override
532    public void run() {
533      try {
534        flushInMemory();
535      } catch (IOException e) {
536        LOG.warn("Unable to run memstore compaction. region "
537            + getRegionServices().getRegionInfo().getRegionNameAsString()
538            + "store: "+ getFamilyName(), e);
539      }
540    }
541  }
542
543  @VisibleForTesting
544  boolean isMemStoreFlushingInMemory() {
545    return inMemoryFlushInProgress.get();
546  }
547
548  /**
549   * @param cell Find the row that comes after this one.  If null, we return the
550   *             first.
551   * @return Next row or null if none found.
552   */
553  Cell getNextRow(final Cell cell) {
554    Cell lowest = null;
555    List<Segment> segments = getSegments();
556    for (Segment segment : segments) {
557      if (lowest == null) {
558        lowest = getNextRow(cell, segment.getCellSet());
559      } else {
560        lowest = getLowest(lowest, getNextRow(cell, segment.getCellSet()));
561      }
562    }
563    return lowest;
564  }
565
566  @VisibleForTesting
567  long getInmemoryFlushSize() {
568    return inmemoryFlushSize;
569  }
570
571  // debug method
572  public void debug() {
573    String msg = "active size=" + this.active.getDataSize();
574    msg += " in-memory flush size is "+ inmemoryFlushSize;
575    msg += " allow compaction is "+ (allowCompaction.get() ? "true" : "false");
576    msg += " inMemoryFlushInProgress is "+ (inMemoryFlushInProgress.get() ? "true" : "false");
577    LOG.debug(msg);
578  }
579
580}