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.mob;
019
020import static org.apache.hadoop.hbase.regionserver.ScanType.COMPACT_DROP_DELETES;
021import static org.apache.hadoop.hbase.regionserver.ScanType.COMPACT_RETAIN_DELETES;
022
023import java.io.FileNotFoundException;
024import java.io.IOException;
025import java.io.InterruptedIOException;
026import java.util.ArrayList;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map.Entry;
031import java.util.Optional;
032import java.util.function.Consumer;
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.fs.FileStatus;
035import org.apache.hadoop.fs.FileSystem;
036import org.apache.hadoop.fs.Path;
037import org.apache.hadoop.hbase.DoNotRetryIOException;
038import org.apache.hadoop.hbase.ExtendedCell;
039import org.apache.hadoop.hbase.KeyValue;
040import org.apache.hadoop.hbase.KeyValueUtil;
041import org.apache.hadoop.hbase.PrivateCellUtil;
042import org.apache.hadoop.hbase.TableName;
043import org.apache.hadoop.hbase.regionserver.CellSink;
044import org.apache.hadoop.hbase.regionserver.HMobStore;
045import org.apache.hadoop.hbase.regionserver.HStore;
046import org.apache.hadoop.hbase.regionserver.HStoreFile;
047import org.apache.hadoop.hbase.regionserver.InternalScanner;
048import org.apache.hadoop.hbase.regionserver.KeyValueScanner;
049import org.apache.hadoop.hbase.regionserver.ScanInfo;
050import org.apache.hadoop.hbase.regionserver.ScanType;
051import org.apache.hadoop.hbase.regionserver.ScannerContext;
052import org.apache.hadoop.hbase.regionserver.ShipperListener;
053import org.apache.hadoop.hbase.regionserver.StoreFileScanner;
054import org.apache.hadoop.hbase.regionserver.StoreFileWriter;
055import org.apache.hadoop.hbase.regionserver.StoreScanner;
056import org.apache.hadoop.hbase.regionserver.compactions.CloseChecker;
057import org.apache.hadoop.hbase.regionserver.compactions.CompactionProgress;
058import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequestImpl;
059import org.apache.hadoop.hbase.regionserver.compactions.DefaultCompactor;
060import org.apache.hadoop.hbase.regionserver.throttle.ThroughputControlUtil;
061import org.apache.hadoop.hbase.regionserver.throttle.ThroughputController;
062import org.apache.hadoop.hbase.security.User;
063import org.apache.hadoop.hbase.util.Bytes;
064import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
065import org.apache.yetus.audience.InterfaceAudience;
066import org.slf4j.Logger;
067import org.slf4j.LoggerFactory;
068
069import org.apache.hbase.thirdparty.com.google.common.collect.HashMultimap;
070import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableSetMultimap;
071import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
072import org.apache.hbase.thirdparty.com.google.common.collect.SetMultimap;
073
074/**
075 * Compact passed set of files in the mob-enabled column family.
076 */
077@InterfaceAudience.Private
078public class DefaultMobStoreCompactor extends DefaultCompactor {
079
080  private static final Logger LOG = LoggerFactory.getLogger(DefaultMobStoreCompactor.class);
081  protected long mobSizeThreshold;
082  protected HMobStore mobStore;
083  protected boolean ioOptimizedMode = false;
084  protected final boolean cacheMobBlocksOnCompaction;
085
086  /*
087   * MOB file reference set thread local variable. It contains set of a MOB file names, which newly
088   * compacted store file has references to. This variable is populated during compaction and the
089   * content of it is written into meta section of a newly created store file at the final step of
090   * compaction process.
091   */
092  static ThreadLocal<SetMultimap<TableName, String>> mobRefSet =
093    ThreadLocal.withInitial(HashMultimap::create);
094
095  /*
096   * Is it user or system-originated request.
097   */
098
099  static ThreadLocal<Boolean> userRequest = new ThreadLocal<Boolean>() {
100    @Override
101    protected Boolean initialValue() {
102      return Boolean.FALSE;
103    }
104  };
105
106  /*
107   * Disable IO mode. IO mode can be forcefully disabled if compactor finds old MOB file
108   * (pre-distributed compaction). This means that migration has not been completed yet. During data
109   * migration (upgrade) process only general compaction is allowed.
110   */
111
112  static ThreadLocal<Boolean> disableIO = new ThreadLocal<Boolean>() {
113
114    @Override
115    protected Boolean initialValue() {
116      return Boolean.FALSE;
117    }
118  };
119
120  /*
121   * Map : MOB file name - file length Can be expensive for large amount of MOB files.
122   */
123  static ThreadLocal<HashMap<String, Long>> mobLengthMap =
124    new ThreadLocal<HashMap<String, Long>>() {
125      @Override
126      protected HashMap<String, Long> initialValue() {
127        return new HashMap<String, Long>();
128      }
129    };
130
131  private final InternalScannerFactory scannerFactory = new InternalScannerFactory() {
132
133    @Override
134    public ScanType getScanType(CompactionRequestImpl request) {
135      return request.isAllFiles() ? COMPACT_DROP_DELETES : COMPACT_RETAIN_DELETES;
136    }
137
138    @Override
139    public InternalScanner createScanner(ScanInfo scanInfo, List<StoreFileScanner> scanners,
140      ScanType scanType, FileDetails fd, long smallestReadPoint) throws IOException {
141      return new StoreScanner(store, scanInfo, scanners, scanType, smallestReadPoint,
142        fd.earliestPutTs);
143    }
144  };
145
146  private final CellSinkFactory<StoreFileWriter> writerFactory =
147    new CellSinkFactory<StoreFileWriter>() {
148      @Override
149      public StoreFileWriter createWriter(InternalScanner scanner,
150        org.apache.hadoop.hbase.regionserver.compactions.Compactor.FileDetails fd,
151        boolean shouldDropBehind, boolean major, Consumer<Path> writerCreationTracker)
152        throws IOException {
153        // make this writer with tags always because of possible new cells with tags.
154        return store.getStoreEngine()
155          .createWriter(createParams(fd, shouldDropBehind, major, writerCreationTracker)
156            .includeMVCCReadpoint(true).includesTag(true));
157      }
158    };
159
160  public DefaultMobStoreCompactor(Configuration conf, HStore store) {
161    super(conf, store);
162    // The mob cells reside in the mob-enabled column family which is held by HMobStore.
163    // During the compaction, the compactor reads the cells from the mob files and
164    // probably creates new mob files. All of these operations are included in HMobStore,
165    // so we need to cast the Store to HMobStore.
166    if (!(store instanceof HMobStore)) {
167      throw new IllegalArgumentException("The store " + store + " is not a HMobStore");
168    }
169    this.mobStore = (HMobStore) store;
170    this.mobSizeThreshold = store.getColumnFamilyDescriptor().getMobThreshold();
171    this.ioOptimizedMode =
172      conf.get(MobConstants.MOB_COMPACTION_TYPE_KEY, MobConstants.DEFAULT_MOB_COMPACTION_TYPE)
173        .equals(MobConstants.OPTIMIZED_MOB_COMPACTION_TYPE);
174    this.cacheMobBlocksOnCompaction = conf.getBoolean(MobConstants.MOB_COMPACTION_READ_CACHE_BLOCKS,
175      MobConstants.DEFAULT_MOB_COMPACTION_READ_CACHE_BLOCKS);
176  }
177
178  /**
179   * Resolves a MOB reference cell to its backing MOB value and returns an independent,
180   * heap-resident copy of the resolved cell.
181   * <p>
182   * A MOB cell resolved from a MOB file is backed by a {@code StoreFileScanner}; closing the
183   * {@link MobCell} closes that scanner and may release/recycle the NIO buffers referenced by the
184   * returned cell. We close the {@link MobCell} here to avoid leaking scanners/buffers while
185   * compacting many reference cells.
186   * <p>
187   * The {@link KeyValueUtil#copyToNewKeyValue(ExtendedCell)} call is required by this ownership
188   * model: HFile writers and encoders may retain references to appended cells (e.g.
189   * {@code lastCell}, {@code firstCellInBlock}, and the data block encoder's {@code prevCell})
190   * until {@code beforeShipped()}. Returning the scanner-backed cell directly would let those later
191   * reads access released buffers. Removing this copy would require changing the caller to retain
192   * each {@link MobCell} and close it only after the writers have shipped their retained
193   * references.
194   */
195  protected ExtendedCell resolveMobCell(ExtendedCell reference) throws IOException {
196    try (MobCell mobCell = mobStore.resolve(reference, cacheMobBlocksOnCompaction, false)) {
197      return KeyValueUtil.copyToNewKeyValue(mobCell.getCell());
198    }
199  }
200
201  @Override
202  public List<Path> compact(CompactionRequestImpl request,
203    ThroughputController throughputController, User user) throws IOException {
204    String tableName = store.getTableName().toString();
205    String regionName = store.getRegionInfo().getRegionNameAsString();
206    String familyName = store.getColumnFamilyName();
207    LOG.info(
208      "MOB compaction: major={} isAll={} priority={} throughput controller={}"
209        + " table={} cf={} region={}",
210      request.isMajor(), request.isAllFiles(), request.getPriority(), throughputController,
211      tableName, familyName, regionName);
212    if (request.getPriority() == HStore.PRIORITY_USER) {
213      userRequest.set(Boolean.TRUE);
214    } else {
215      userRequest.set(Boolean.FALSE);
216    }
217    LOG.debug("MOB compaction table={} cf={} region={} files: {}", tableName, familyName,
218      regionName, request.getFiles());
219    // Check if I/O optimized MOB compaction
220    if (ioOptimizedMode) {
221      if (request.isMajor() && request.getPriority() == HStore.PRIORITY_USER) {
222        try {
223          final SetMultimap<TableName, String> mobRefs = request.getFiles().stream().map(file -> {
224            byte[] value = file.getMetadataValue(HStoreFile.MOB_FILE_REFS);
225            ImmutableSetMultimap.Builder<TableName, String> builder;
226            if (value == null) {
227              builder = ImmutableSetMultimap.builder();
228            } else {
229              try {
230                builder = MobUtils.deserializeMobFileRefs(value);
231              } catch (RuntimeException exception) {
232                throw new RuntimeException("failure getting mob references for hfile " + file,
233                  exception);
234              }
235            }
236            return builder;
237          }).reduce((a, b) -> a.putAll(b.build())).orElseGet(ImmutableSetMultimap::builder).build();
238          // reset disableIO
239          disableIO.set(Boolean.FALSE);
240          if (!mobRefs.isEmpty()) {
241            calculateMobLengthMap(mobRefs);
242          }
243          LOG.info(
244            "Table={} cf={} region={}. I/O optimized MOB compaction. "
245              + "Total referenced MOB files: {}",
246            tableName, familyName, regionName, mobRefs.size());
247        } catch (RuntimeException exception) {
248          throw new IOException("Failed to get list of referenced hfiles for request " + request,
249            exception);
250        }
251      }
252    }
253
254    return compact(request, scannerFactory, writerFactory, throughputController, user);
255  }
256
257  /**
258   * @param mobRefs multimap of original table name -> mob hfile
259   */
260  private void calculateMobLengthMap(SetMultimap<TableName, String> mobRefs) throws IOException {
261    FileSystem fs = store.getFileSystem();
262    HashMap<String, Long> map = mobLengthMap.get();
263    map.clear();
264    for (Entry<TableName, String> reference : mobRefs.entries()) {
265      final TableName table = reference.getKey();
266      final String mobfile = reference.getValue();
267      if (MobFileName.isOldMobFileName(mobfile)) {
268        disableIO.set(Boolean.TRUE);
269      }
270      List<Path> locations = mobStore.getLocations(table);
271      for (Path p : locations) {
272        try {
273          FileStatus st = fs.getFileStatus(new Path(p, mobfile));
274          long size = st.getLen();
275          LOG.debug("Referenced MOB file={} size={}", mobfile, size);
276          map.put(mobfile, size);
277          break;
278        } catch (FileNotFoundException exception) {
279          LOG.debug("Mob file {} was not in location {}. May have other locations to try.", mobfile,
280            p);
281        }
282      }
283      if (!map.containsKey(mobfile)) {
284        throw new FileNotFoundException("Could not find mob file " + mobfile + " in the list of "
285          + "expected locations: " + locations);
286      }
287    }
288  }
289
290  /**
291   * Performs compaction on a column family with the mob flag enabled. This works only when MOB
292   * compaction is explicitly requested (by User), or by Master There are two modes of a MOB
293   * compaction:<br>
294   * <p>
295   * <ul>
296   * <li>1. Full mode - when all MOB data for a region is compacted into a single MOB file.
297   * <li>2. I/O optimized mode - for use cases with no or infrequent updates/deletes of a <br>
298   * MOB data. The main idea behind i/o optimized compaction is to limit maximum size of a MOB file
299   * produced during compaction and to limit I/O write/read amplification.
300   * </ul>
301   * The basic algorithm of compaction is the following: <br>
302   * 1. If the Put cell has a mob reference tag, the cell's value is the path of the mob file.
303   * <ol>
304   * <li>If the value size of a cell is larger than the threshold, this cell is regarded as a mob,
305   * directly copy the (with mob tag) cell into the new store file.</li>
306   * <li>Otherwise, retrieve the mob cell from the mob file, and writes a copy of the cell into the
307   * new store file.</li>
308   * </ol>
309   * 2. If the Put cell doesn't have a reference tag.
310   * <ol>
311   * <li>If the value size of a cell is larger than the threshold, this cell is regarded as a mob,
312   * write this cell to a mob file, and write the path of this mob file to the store file.</li>
313   * <li>Otherwise, directly write this cell into the store file.</li>
314   * </ol>
315   * @param fd                   File details
316   * @param scanner              Where to read from.
317   * @param writer               Where to write to.
318   * @param smallestReadPoint    Smallest read point.
319   * @param cleanSeqId           When true, remove seqId(used to be mvcc) value which is <=
320   *                             smallestReadPoint
321   * @param throughputController The compaction throughput controller.
322   * @param request              compaction request.
323   * @param progress             Progress reporter.
324   * @return Whether compaction ended; false if it was interrupted for any reason.
325   */
326  @Override
327  protected boolean performCompaction(FileDetails fd, InternalScanner scanner, CellSink writer,
328    long smallestReadPoint, boolean cleanSeqId, ThroughputController throughputController,
329    CompactionRequestImpl request, CompactionProgress progress) throws IOException {
330    long bytesWrittenProgressForLog = 0;
331    long bytesWrittenProgressForShippedCall = 0;
332    // Clear old mob references
333    mobRefSet.get().clear();
334    boolean isUserRequest = userRequest.get();
335    boolean major = request.isAllFiles();
336    boolean compactMOBs = major && isUserRequest;
337    boolean discardMobMiss = conf.getBoolean(MobConstants.MOB_UNSAFE_DISCARD_MISS_KEY,
338      MobConstants.DEFAULT_MOB_DISCARD_MISS);
339    if (discardMobMiss) {
340      LOG.warn("{}=true. This is unsafe setting recommended only when first upgrading to a version"
341        + " with the distributed mob compaction feature on a cluster that has experienced MOB data "
342        + "corruption.", MobConstants.MOB_UNSAFE_DISCARD_MISS_KEY);
343    }
344    long maxMobFileSize = conf.getLong(MobConstants.MOB_COMPACTION_MAX_FILE_SIZE_KEY,
345      MobConstants.DEFAULT_MOB_COMPACTION_MAX_FILE_SIZE);
346    boolean ioOptimizedMode = this.ioOptimizedMode && !disableIO.get();
347    LOG.info(
348      "Compact MOB={} optimized configured={} optimized enabled={} maximum MOB file size={}"
349        + " major={} store={}",
350      compactMOBs, this.ioOptimizedMode, ioOptimizedMode, maxMobFileSize, major, getStoreInfo());
351    // Since scanner.next() can return 'false' but still be delivering data,
352    // we have to use a do/while loop.
353    List<ExtendedCell> cells = new ArrayList<>();
354    // Limit to "hbase.hstore.compaction.kv.max" (default 10) to avoid OOME
355    long currentTime = EnvironmentEdgeManager.currentTime();
356    long lastMillis = 0;
357    if (LOG.isDebugEnabled()) {
358      lastMillis = currentTime;
359    }
360    CloseChecker closeChecker = new CloseChecker(conf, currentTime);
361    String compactionName = ThroughputControlUtil.getNameForThrottling(store, "compaction");
362    long now = 0;
363    boolean hasMore;
364    byte[] fileName = null;
365    StoreFileWriter mobFileWriter = null;
366    /*
367     * mobCells are used only to decide if we need to commit or abort current MOB output file.
368     */
369    long mobCells = 0;
370    long cellsCountCompactedToMob = 0, cellsCountCompactedFromMob = 0;
371    long cellsSizeCompactedToMob = 0, cellsSizeCompactedFromMob = 0;
372    boolean finished = false;
373
374    ScannerContext scannerContext = ScannerContext.newBuilder().setBatchLimit(compactionKVMax)
375      .setSizeLimit(ScannerContext.LimitScope.BETWEEN_CELLS, Long.MAX_VALUE, Long.MAX_VALUE,
376        compactScannerSizeLimit)
377      .build();
378    throughputController.start(compactionName);
379    KeyValueScanner kvs = (scanner instanceof KeyValueScanner) ? (KeyValueScanner) scanner : null;
380    long shippedCallSizeLimit = Math.min(compactScannerSizeLimit,
381      (long) request.getFiles().size() * this.store.getColumnFamilyDescriptor().getBlocksize());
382
383    ExtendedCell mobCell = null;
384    List<String> committedMobWriterFileNames = new ArrayList<>();
385    try {
386
387      mobFileWriter = newMobWriter(fd, major, request.getWriterCreationTracker());
388      fileName = Bytes.toBytes(mobFileWriter.getPath().getName());
389
390      do {
391        hasMore = scanner.next(cells, scannerContext);
392        currentTime = EnvironmentEdgeManager.currentTime();
393        if (LOG.isDebugEnabled()) {
394          now = currentTime;
395        }
396        if (closeChecker.isTimeLimit(store, currentTime)) {
397          progress.cancel();
398          return false;
399        }
400        for (ExtendedCell c : cells) {
401          if (compactMOBs) {
402            if (MobUtils.isMobReferenceCell(c)) {
403              String fName = MobUtils.getMobFileName(c);
404              // Added to support migration
405              try {
406                mobCell = resolveMobCell(c);
407              } catch (DoNotRetryIOException e) {
408                if (
409                  discardMobMiss && e.getCause() != null
410                    && e.getCause() instanceof FileNotFoundException
411                ) {
412                  LOG.error("Missing MOB cell: file={} not found cell={}", fName, c);
413                  continue;
414                } else {
415                  throw e;
416                }
417              }
418
419              if (discardMobMiss && mobCell.getValueLength() == 0) {
420                LOG.error("Missing MOB cell value: file={} mob cell={} cell={}", fName, mobCell, c);
421                continue;
422              } else if (mobCell.getValueLength() == 0) {
423                String errMsg =
424                  String.format("Found 0 length MOB cell in a file=%s mob cell=%s " + " cell=%s",
425                    fName, mobCell, c);
426                throw new IOException(errMsg);
427              }
428
429              if (mobCell.getValueLength() > mobSizeThreshold) {
430                // put the mob data back to the MOB store file
431                PrivateCellUtil.setSequenceId(mobCell, c.getSequenceId());
432                if (!ioOptimizedMode) {
433                  mobFileWriter.append(mobCell);
434                  mobCells++;
435                  writer.append(
436                    MobUtils.createMobRefCell(mobCell, fileName, this.mobStore.getRefCellTags()));
437                } else {
438                  // I/O optimized mode
439                  // Check if MOB cell origin file size is
440                  // greater than threshold
441                  Long size = mobLengthMap.get().get(fName);
442                  if (size == null) {
443                    // FATAL error (we should never get here though), abort compaction
444                    // This error means that meta section of store file does not contain
445                    // MOB file, which has references in at least one cell from this store file
446                    String msg = String.format(
447                      "Found an unexpected MOB file during compaction %s, aborting compaction %s",
448                      fName, getStoreInfo());
449                    throw new IOException(msg);
450                  }
451                  // Can not be null
452                  if (size < maxMobFileSize) {
453                    // If MOB cell origin file is below threshold
454                    // it is get compacted
455                    mobFileWriter.append(mobCell);
456                    // Update number of mobCells in a current mob writer
457                    mobCells++;
458                    writer.append(
459                      MobUtils.createMobRefCell(mobCell, fileName, this.mobStore.getRefCellTags()));
460                    // Update total size of the output (we do not take into account
461                    // file compression yet)
462                    long len = mobFileWriter.getPos();
463                    if (len > maxMobFileSize) {
464                      LOG.debug("Closing output MOB File, length={} file={}, store={}", len,
465                        mobFileWriter.getPath().getName(), getStoreInfo());
466                      mobFileWriter = switchToNewMobWriter(mobFileWriter, fd, mobCells, major,
467                        request, committedMobWriterFileNames);
468                      fileName = Bytes.toBytes(mobFileWriter.getPath().getName());
469                      mobCells = 0;
470                    }
471                  } else {
472                    // We leave large MOB file as is (is not compacted),
473                    // then we update set of MOB file references
474                    // and append mob cell directly to the store's writer
475                    Optional<TableName> refTable = MobUtils.getTableName(c);
476                    if (refTable.isPresent()) {
477                      mobRefSet.get().put(refTable.get(), fName);
478                      writer.append(c);
479                    } else {
480                      throw new IOException(String.format("MOB cell did not contain a tablename "
481                        + "tag. should not be possible. see ref guide on mob troubleshooting. "
482                        + "store=%s cell=%s", getStoreInfo(), c));
483                    }
484                  }
485                }
486              } else {
487                // If MOB value is less than threshold, append it directly to a store file
488                PrivateCellUtil.setSequenceId(mobCell, c.getSequenceId());
489                writer.append(mobCell);
490                cellsCountCompactedFromMob++;
491                cellsSizeCompactedFromMob += mobCell.getValueLength();
492              }
493            } else {
494              // Not a MOB reference cell
495              int size = c.getValueLength();
496              if (size > mobSizeThreshold) {
497                // This MOB cell comes from a regular store file
498                // therefore we store it into original mob output
499                mobFileWriter.append(c);
500                writer
501                  .append(MobUtils.createMobRefCell(c, fileName, this.mobStore.getRefCellTags()));
502                mobCells++;
503                cellsCountCompactedToMob++;
504                cellsSizeCompactedToMob += c.getValueLength();
505                if (ioOptimizedMode) {
506                  // Update total size of the output (we do not take into account
507                  // file compression yet)
508                  long len = mobFileWriter.getPos();
509                  if (len > maxMobFileSize) {
510                    mobFileWriter = switchToNewMobWriter(mobFileWriter, fd, mobCells, major,
511                      request, committedMobWriterFileNames);
512                    fileName = Bytes.toBytes(mobFileWriter.getPath().getName());
513                    mobCells = 0;
514                  }
515                }
516              } else {
517                // Not a MOB cell, write it directly to a store file
518                writer.append(c);
519              }
520            }
521          } else if (c.getTypeByte() != KeyValue.Type.Put.getCode()) {
522            // Not a major compaction or major with MOB disabled
523            // If the kv type is not put, directly write the cell
524            // to the store file.
525            writer.append(c);
526          } else if (MobUtils.isMobReferenceCell(c)) {
527            // Not a major MOB compaction, Put MOB reference
528            if (MobUtils.hasValidMobRefCellValue(c)) {
529              // We do not check mobSizeThreshold during normal compaction,
530              // leaving it to a MOB compaction run
531              Optional<TableName> refTable = MobUtils.getTableName(c);
532              if (refTable.isPresent()) {
533                mobRefSet.get().put(refTable.get(), MobUtils.getMobFileName(c));
534                writer.append(c);
535              } else {
536                throw new IOException(String.format("MOB cell did not contain a tablename "
537                  + "tag. should not be possible. see ref guide on mob troubleshooting. "
538                  + "store=%s cell=%s", getStoreInfo(), c));
539              }
540            } else {
541              String errMsg = String.format("Corrupted MOB reference: %s", c.toString());
542              throw new IOException(errMsg);
543            }
544          } else if (c.getValueLength() <= mobSizeThreshold) {
545            // If the value size of a cell is not larger than the threshold, directly write it to
546            // the store file.
547            writer.append(c);
548          } else {
549            // If the value size of a cell is larger than the threshold, it's regarded as a mob,
550            // write this cell to a mob file, and write the path to the store file.
551            mobCells++;
552            // append the original keyValue in the mob file.
553            mobFileWriter.append(c);
554            ExtendedCell reference =
555              MobUtils.createMobRefCell(c, fileName, this.mobStore.getRefCellTags());
556            // write the cell whose value is the path of a mob file to the store file.
557            writer.append(reference);
558            cellsCountCompactedToMob++;
559            cellsSizeCompactedToMob += c.getValueLength();
560            if (ioOptimizedMode) {
561              long len = mobFileWriter.getPos();
562              if (len > maxMobFileSize) {
563                mobFileWriter = switchToNewMobWriter(mobFileWriter, fd, mobCells, major, request,
564                  committedMobWriterFileNames);
565                fileName = Bytes.toBytes(mobFileWriter.getPath().getName());
566                mobCells = 0;
567              }
568            }
569          }
570
571          int len = c.getSerializedSize();
572          ++progress.currentCompactedKVs;
573          progress.totalCompactedSize += len;
574          bytesWrittenProgressForShippedCall += len;
575          if (LOG.isDebugEnabled()) {
576            bytesWrittenProgressForLog += len;
577          }
578          throughputController.control(compactionName, len);
579          if (closeChecker.isSizeLimit(store, len)) {
580            progress.cancel();
581            return false;
582          }
583          if (kvs != null && bytesWrittenProgressForShippedCall > shippedCallSizeLimit) {
584            ((ShipperListener) writer).beforeShipped();
585            kvs.shipped();
586            scannerContext.clearBlockSizeProgress();
587            bytesWrittenProgressForShippedCall = 0;
588          }
589        }
590        // Log the progress of long running compactions every minute if
591        // logging at DEBUG level
592        if (LOG.isDebugEnabled()) {
593          if ((now - lastMillis) >= COMPACTION_PROGRESS_LOG_INTERVAL) {
594            String rate = String.format("%.2f",
595              (bytesWrittenProgressForLog / 1024.0) / ((now - lastMillis) / 1000.0));
596            LOG.debug("Compaction progress: {} {}, rate={} KB/sec, throughputController is {}",
597              compactionName, progress, rate, throughputController);
598            lastMillis = now;
599            bytesWrittenProgressForLog = 0;
600          }
601        }
602        cells.clear();
603      } while (hasMore);
604      // Commit last MOB writer
605      commitOrAbortMobWriter(mobFileWriter, fd.maxSeqId, mobCells, major);
606      finished = true;
607    } catch (InterruptedException e) {
608      progress.cancel();
609      throw new InterruptedIOException(
610        "Interrupted while control throughput of compacting " + compactionName);
611    } catch (IOException t) {
612      String msg = "Mob compaction failed for region: " + store.getRegionInfo().getEncodedName();
613      throw new IOException(msg, t);
614    } finally {
615      // Clone last cell in the final because writer will append last cell when committing. If
616      // don't clone here and once the scanner get closed, then the memory of last cell will be
617      // released. (HBASE-22582)
618      ((ShipperListener) writer).beforeShipped();
619      throughputController.finish(compactionName);
620      if (!finished && mobFileWriter != null) {
621        // Remove all MOB references because compaction failed
622        clearThreadLocals();
623        // Abort writer
624        LOG.debug("Aborting writer for {} because of a compaction failure, Store {}",
625          mobFileWriter.getPath(), getStoreInfo());
626        abortWriter(mobFileWriter);
627        deleteCommittedMobFiles(committedMobWriterFileNames);
628      }
629    }
630
631    mobStore.updateCellsCountCompactedFromMob(cellsCountCompactedFromMob);
632    mobStore.updateCellsCountCompactedToMob(cellsCountCompactedToMob);
633    mobStore.updateCellsSizeCompactedFromMob(cellsSizeCompactedFromMob);
634    mobStore.updateCellsSizeCompactedToMob(cellsSizeCompactedToMob);
635    progress.complete();
636    return true;
637  }
638
639  protected String getStoreInfo() {
640    return String.format("[table=%s family=%s region=%s]", store.getTableName().getNameAsString(),
641      store.getColumnFamilyName(), store.getRegionInfo().getEncodedName());
642  }
643
644  private void clearThreadLocals() {
645    mobRefSet.get().clear();
646    HashMap<String, Long> map = mobLengthMap.get();
647    if (map != null) {
648      map.clear();
649    }
650  }
651
652  private StoreFileWriter newMobWriter(FileDetails fd, boolean major,
653    Consumer<Path> writerCreationTracker) throws IOException {
654    try {
655      StoreFileWriter mobFileWriter = mobStore.getStoreEngine().requireWritingToTmpDirFirst()
656        ? mobStore.createWriterInTmp(new Date(fd.latestPutTs), fd.maxKeyCount,
657          major ? majorCompactionCompression : minorCompactionCompression,
658          store.getRegionInfo().getStartKey(), true)
659        : mobStore.createWriter(new Date(fd.latestPutTs), fd.maxKeyCount,
660          major ? majorCompactionCompression : minorCompactionCompression,
661          store.getRegionInfo().getStartKey(), true, writerCreationTracker);
662      LOG.debug("New MOB writer created={} store={}", mobFileWriter.getPath().getName(),
663        getStoreInfo());
664      // Add reference we get for compact MOB
665      mobRefSet.get().put(store.getTableName(), mobFileWriter.getPath().getName());
666      return mobFileWriter;
667    } catch (IOException e) {
668      // Bailing out
669      throw new IOException(String.format("Failed to create mob writer, store=%s", getStoreInfo()),
670        e);
671    }
672  }
673
674  private void commitOrAbortMobWriter(StoreFileWriter mobFileWriter, long maxSeqId, long mobCells,
675    boolean major) throws IOException {
676    // Commit or abort major mob writer
677    // If IOException happens during below operation, some
678    // MOB files can be committed partially, but corresponding
679    // store file won't be committed, therefore these MOB files
680    // become orphans and will be deleted during next MOB cleaning chore cycle
681
682    if (mobFileWriter != null) {
683      LOG.debug("Commit or abort size={} mobCells={} major={} file={}, store={}",
684        mobFileWriter.getPos(), mobCells, major, mobFileWriter.getPath().getName(), getStoreInfo());
685      Path path =
686        MobUtils.getMobFamilyPath(conf, store.getTableName(), store.getColumnFamilyName());
687      if (mobCells > 0) {
688        // If the mob file is not empty, commit it.
689        mobFileWriter.appendMetadata(maxSeqId, major, mobCells);
690        mobFileWriter.close();
691        mobStore.commitFile(mobFileWriter.getPath(), path);
692      } else {
693        // If the mob file is empty, delete it instead of committing.
694        LOG.debug("Aborting writer for {} because there are no MOB cells, store={}",
695          mobFileWriter.getPath(), getStoreInfo());
696        // Remove MOB file from reference set
697        mobRefSet.get().remove(store.getTableName(), mobFileWriter.getPath().getName());
698        abortWriter(mobFileWriter);
699      }
700    } else {
701      LOG.debug("Mob file writer is null, skipping commit/abort, store=", getStoreInfo());
702    }
703  }
704
705  @Override
706  protected List<Path> commitWriter(StoreFileWriter writer, FileDetails fd,
707    CompactionRequestImpl request) throws IOException {
708    List<Path> newFiles = Lists.newArrayList(writer.getPath());
709    writer.appendMetadata(fd.maxSeqId, request.isAllFiles(), request.getFiles());
710    writer.appendMobMetadata(mobRefSet.get());
711    writer.close();
712    clearThreadLocals();
713    return newFiles;
714  }
715
716  private StoreFileWriter switchToNewMobWriter(StoreFileWriter mobFileWriter, FileDetails fd,
717    long mobCells, boolean major, CompactionRequestImpl request,
718    List<String> committedMobWriterFileNames) throws IOException {
719    commitOrAbortMobWriter(mobFileWriter, fd.maxSeqId, mobCells, major);
720    committedMobWriterFileNames.add(mobFileWriter.getPath().getName());
721    return newMobWriter(fd, major, request.getWriterCreationTracker());
722  }
723
724  private void deleteCommittedMobFiles(List<String> fileNames) {
725    if (fileNames.isEmpty()) {
726      return;
727    }
728    Path mobColumnFamilyPath =
729      MobUtils.getMobFamilyPath(conf, store.getTableName(), store.getColumnFamilyName());
730    for (String fileName : fileNames) {
731      if (fileName == null) {
732        continue;
733      }
734      Path path = new Path(mobColumnFamilyPath, fileName);
735      try {
736        if (store.getFileSystem().exists(path)) {
737          store.getFileSystem().delete(path, false);
738        }
739      } catch (IOException e) {
740        LOG.warn("Failed to delete the mob file {} for an failed mob compaction.", path, e);
741      }
742    }
743
744  }
745
746}