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.wal;
019
020import java.io.EOFException;
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.io.InterruptedIOException;
024import java.text.ParseException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.NavigableSet;
032import java.util.Set;
033import java.util.TreeMap;
034import java.util.TreeSet;
035import java.util.UUID;
036import java.util.concurrent.Callable;
037import java.util.concurrent.CompletionService;
038import java.util.concurrent.ConcurrentHashMap;
039import java.util.concurrent.ExecutionException;
040import java.util.concurrent.ExecutorCompletionService;
041import java.util.concurrent.Future;
042import java.util.concurrent.ThreadFactory;
043import java.util.concurrent.ThreadPoolExecutor;
044import java.util.concurrent.TimeUnit;
045import java.util.concurrent.atomic.AtomicLong;
046import java.util.concurrent.atomic.AtomicReference;
047import java.util.regex.Matcher;
048import java.util.regex.Pattern;
049import org.apache.commons.lang3.ArrayUtils;
050import org.apache.hadoop.conf.Configuration;
051import org.apache.hadoop.fs.FileAlreadyExistsException;
052import org.apache.hadoop.fs.FileStatus;
053import org.apache.hadoop.fs.FileSystem;
054import org.apache.hadoop.fs.Path;
055import org.apache.hadoop.fs.PathFilter;
056import org.apache.hadoop.hbase.Cell;
057import org.apache.hadoop.hbase.CellScanner;
058import org.apache.hadoop.hbase.CellUtil;
059import org.apache.hadoop.hbase.HBaseConfiguration;
060import org.apache.hadoop.hbase.HConstants;
061import org.apache.hadoop.hbase.TableName;
062import org.apache.hadoop.hbase.client.Delete;
063import org.apache.hadoop.hbase.client.Durability;
064import org.apache.hadoop.hbase.client.Mutation;
065import org.apache.hadoop.hbase.client.Put;
066import org.apache.hadoop.hbase.client.RegionInfo;
067import org.apache.hadoop.hbase.coordination.SplitLogWorkerCoordination;
068import org.apache.hadoop.hbase.io.HeapSize;
069import org.apache.hadoop.hbase.log.HBaseMarkers;
070import org.apache.hadoop.hbase.master.SplitLogManager;
071import org.apache.hadoop.hbase.monitoring.MonitoredTask;
072import org.apache.hadoop.hbase.monitoring.TaskMonitor;
073import org.apache.hadoop.hbase.regionserver.HRegion;
074import org.apache.hadoop.hbase.regionserver.LastSequenceId;
075import org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL;
076import org.apache.hadoop.hbase.regionserver.wal.WALCellCodec;
077import org.apache.hadoop.hbase.util.Bytes;
078import org.apache.hadoop.hbase.util.CancelableProgressable;
079import org.apache.hadoop.hbase.util.ClassSize;
080import org.apache.hadoop.hbase.util.FSUtils;
081import org.apache.hadoop.hbase.util.Pair;
082import org.apache.hadoop.hbase.util.Threads;
083import org.apache.hadoop.hbase.wal.WAL.Entry;
084import org.apache.hadoop.hbase.wal.WAL.Reader;
085import org.apache.hadoop.hbase.wal.WALProvider.Writer;
086import org.apache.hadoop.hbase.zookeeper.ZKSplitLog;
087import org.apache.hadoop.io.MultipleIOException;
088import org.apache.hadoop.ipc.RemoteException;
089import org.apache.yetus.audience.InterfaceAudience;
090import org.slf4j.Logger;
091import org.slf4j.LoggerFactory;
092
093import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
094import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
095import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
096import org.apache.hbase.thirdparty.com.google.protobuf.TextFormat;
097import org.apache.hbase.thirdparty.org.apache.commons.collections4.CollectionUtils;
098import org.apache.hbase.thirdparty.org.apache.commons.collections4.MapUtils;
099
100import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.WALEntry;
101import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.MutationProto.MutationType;
102import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos.RegionStoreSequenceIds;
103import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos.StoreSequenceId;
104import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
105/**
106 * This class is responsible for splitting up a bunch of regionserver commit log
107 * files that are no longer being written to, into new files, one per region, for
108 * recovering data on startup. Delete the old log files when finished.
109 */
110@InterfaceAudience.Private
111public class WALSplitter {
112  private static final Logger LOG = LoggerFactory.getLogger(WALSplitter.class);
113
114  /** By default we retry errors in splitting, rather than skipping. */
115  public static final boolean SPLIT_SKIP_ERRORS_DEFAULT = false;
116
117  // Parameters for split process
118  protected final Path rootDir;
119  protected final FileSystem fs;
120  protected final Configuration conf;
121
122  // Major subcomponents of the split process.
123  // These are separated into inner classes to make testing easier.
124  OutputSink outputSink;
125  private EntryBuffers entryBuffers;
126
127  private SplitLogWorkerCoordination splitLogWorkerCoordination;
128  private final WALFactory walFactory;
129
130  private MonitoredTask status;
131
132  // For checking the latest flushed sequence id
133  protected final LastSequenceId sequenceIdChecker;
134
135  // Map encodedRegionName -> lastFlushedSequenceId
136  protected Map<String, Long> lastFlushedSequenceIds = new ConcurrentHashMap<>();
137
138  // Map encodedRegionName -> maxSeqIdInStores
139  protected Map<String, Map<byte[], Long>> regionMaxSeqIdInStores = new ConcurrentHashMap<>();
140
141  // the file being split currently
142  private FileStatus fileBeingSplit;
143
144  // if we limit the number of writers opened for sinking recovered edits
145  private final boolean splitWriterCreationBounded;
146
147  public final static String SPLIT_WRITER_CREATION_BOUNDED = "hbase.split.writer.creation.bounded";
148
149
150  @VisibleForTesting
151  WALSplitter(final WALFactory factory, Configuration conf, Path rootDir,
152      FileSystem fs, LastSequenceId idChecker,
153      SplitLogWorkerCoordination splitLogWorkerCoordination) {
154    this.conf = HBaseConfiguration.create(conf);
155    String codecClassName = conf
156        .get(WALCellCodec.WAL_CELL_CODEC_CLASS_KEY, WALCellCodec.class.getName());
157    this.conf.set(HConstants.RPC_CODEC_CONF_KEY, codecClassName);
158    this.rootDir = rootDir;
159    this.fs = fs;
160    this.sequenceIdChecker = idChecker;
161    this.splitLogWorkerCoordination = splitLogWorkerCoordination;
162
163    this.walFactory = factory;
164    PipelineController controller = new PipelineController();
165
166    this.splitWriterCreationBounded = conf.getBoolean(SPLIT_WRITER_CREATION_BOUNDED, false);
167
168    entryBuffers = new EntryBuffers(controller,
169        this.conf.getInt("hbase.regionserver.hlog.splitlog.buffersize", 128 * 1024 * 1024),
170        splitWriterCreationBounded);
171
172    int numWriterThreads = this.conf.getInt("hbase.regionserver.hlog.splitlog.writer.threads", 3);
173    if(splitWriterCreationBounded){
174      outputSink = new BoundedLogWriterCreationOutputSink(
175          controller, entryBuffers, numWriterThreads);
176    }else {
177      outputSink = new LogRecoveredEditsOutputSink(controller, entryBuffers, numWriterThreads);
178    }
179  }
180
181  /**
182   * Splits a WAL file into region's recovered-edits directory.
183   * This is the main entry point for distributed log splitting from SplitLogWorker.
184   * <p>
185   * If the log file has N regions then N recovered.edits files will be produced.
186   * <p>
187   * @return false if it is interrupted by the progress-able.
188   */
189  public static boolean splitLogFile(Path rootDir, FileStatus logfile, FileSystem fs,
190      Configuration conf, CancelableProgressable reporter, LastSequenceId idChecker,
191      SplitLogWorkerCoordination splitLogWorkerCoordination, final WALFactory factory)
192      throws IOException {
193    WALSplitter s = new WALSplitter(factory, conf, rootDir, fs, idChecker,
194        splitLogWorkerCoordination);
195    return s.splitLogFile(logfile, reporter);
196  }
197
198  // A wrapper to split one log folder using the method used by distributed
199  // log splitting. Used by tools and unit tests. It should be package private.
200  // It is public only because TestWALObserver is in a different package,
201  // which uses this method to do log splitting.
202  @VisibleForTesting
203  public static List<Path> split(Path rootDir, Path logDir, Path oldLogDir,
204      FileSystem fs, Configuration conf, final WALFactory factory) throws IOException {
205    final FileStatus[] logfiles = SplitLogManager.getFileList(conf,
206        Collections.singletonList(logDir), null);
207    List<Path> splits = new ArrayList<>();
208    if (ArrayUtils.isNotEmpty(logfiles)) {
209      for (FileStatus logfile: logfiles) {
210        WALSplitter s = new WALSplitter(factory, conf, rootDir, fs, null, null);
211        if (s.splitLogFile(logfile, null)) {
212          finishSplitLogFile(rootDir, oldLogDir, logfile.getPath(), conf);
213          if (s.outputSink.splits != null) {
214            splits.addAll(s.outputSink.splits);
215          }
216        }
217      }
218    }
219    if (!fs.delete(logDir, true)) {
220      throw new IOException("Unable to delete src dir: " + logDir);
221    }
222    return splits;
223  }
224
225  /**
226   * log splitting implementation, splits one log file.
227   * @param logfile should be an actual log file.
228   */
229  @VisibleForTesting
230  boolean splitLogFile(FileStatus logfile, CancelableProgressable reporter) throws IOException {
231    Preconditions.checkState(status == null);
232    Preconditions.checkArgument(logfile.isFile(),
233        "passed in file status is for something other than a regular file.");
234    boolean isCorrupted = false;
235    boolean skipErrors = conf.getBoolean("hbase.hlog.split.skip.errors",
236      SPLIT_SKIP_ERRORS_DEFAULT);
237    int interval = conf.getInt("hbase.splitlog.report.interval.loglines", 1024);
238    Path logPath = logfile.getPath();
239    boolean outputSinkStarted = false;
240    boolean progress_failed = false;
241    int editsCount = 0;
242    int editsSkipped = 0;
243
244    status = TaskMonitor.get().createStatus(
245          "Splitting log file " + logfile.getPath() + "into a temporary staging area.");
246    Reader logFileReader = null;
247    this.fileBeingSplit = logfile;
248    try {
249      long logLength = logfile.getLen();
250      LOG.info("Splitting WAL={}, length={}", logPath, logLength);
251      status.setStatus("Opening log file");
252      if (reporter != null && !reporter.progress()) {
253        progress_failed = true;
254        return false;
255      }
256      logFileReader = getReader(logfile, skipErrors, reporter);
257      if (logFileReader == null) {
258        LOG.warn("Nothing to split in WAL={}", logPath);
259        return true;
260      }
261      int numOpenedFilesBeforeReporting = conf.getInt("hbase.splitlog.report.openedfiles", 3);
262      int numOpenedFilesLastCheck = 0;
263      outputSink.setReporter(reporter);
264      outputSink.startWriterThreads();
265      outputSinkStarted = true;
266      Entry entry;
267      Long lastFlushedSequenceId = -1L;
268      while ((entry = getNextLogLine(logFileReader, logPath, skipErrors)) != null) {
269        byte[] region = entry.getKey().getEncodedRegionName();
270        String encodedRegionNameAsStr = Bytes.toString(region);
271        lastFlushedSequenceId = lastFlushedSequenceIds.get(encodedRegionNameAsStr);
272        if (lastFlushedSequenceId == null) {
273          if (sequenceIdChecker != null) {
274            RegionStoreSequenceIds ids = sequenceIdChecker.getLastSequenceId(region);
275            Map<byte[], Long> maxSeqIdInStores = new TreeMap<>(Bytes.BYTES_COMPARATOR);
276            for (StoreSequenceId storeSeqId : ids.getStoreSequenceIdList()) {
277              maxSeqIdInStores.put(storeSeqId.getFamilyName().toByteArray(),
278                storeSeqId.getSequenceId());
279            }
280            regionMaxSeqIdInStores.put(encodedRegionNameAsStr, maxSeqIdInStores);
281            lastFlushedSequenceId = ids.getLastFlushedSequenceId();
282            if (LOG.isDebugEnabled()) {
283              LOG.debug("DLS Last flushed sequenceid for " + encodedRegionNameAsStr + ": " +
284                  TextFormat.shortDebugString(ids));
285            }
286          }
287          if (lastFlushedSequenceId == null) {
288            lastFlushedSequenceId = -1L;
289          }
290          lastFlushedSequenceIds.put(encodedRegionNameAsStr, lastFlushedSequenceId);
291        }
292        if (lastFlushedSequenceId >= entry.getKey().getSequenceId()) {
293          editsSkipped++;
294          continue;
295        }
296        // Don't send Compaction/Close/Open region events to recovered edit type sinks.
297        if (entry.getEdit().isMetaEdit() && !outputSink.keepRegionEvent(entry)) {
298          editsSkipped++;
299          continue;
300        }
301        entryBuffers.appendEntry(entry);
302        editsCount++;
303        int moreWritersFromLastCheck = this.getNumOpenWriters() - numOpenedFilesLastCheck;
304        // If sufficient edits have passed, check if we should report progress.
305        if (editsCount % interval == 0
306            || moreWritersFromLastCheck > numOpenedFilesBeforeReporting) {
307          numOpenedFilesLastCheck = this.getNumOpenWriters();
308          String countsStr = (editsCount - (editsSkipped + outputSink.getSkippedEdits()))
309              + " edits, skipped " + editsSkipped + " edits.";
310          status.setStatus("Split " + countsStr);
311          if (reporter != null && !reporter.progress()) {
312            progress_failed = true;
313            return false;
314          }
315        }
316      }
317    } catch (InterruptedException ie) {
318      IOException iie = new InterruptedIOException();
319      iie.initCause(ie);
320      throw iie;
321    } catch (CorruptedLogFileException e) {
322      LOG.warn("Could not parse, corrupted WAL={}", logPath, e);
323      if (splitLogWorkerCoordination != null) {
324        // Some tests pass in a csm of null.
325        splitLogWorkerCoordination.markCorrupted(rootDir, logfile.getPath().getName(), fs);
326      } else {
327        // for tests only
328        ZKSplitLog.markCorrupted(rootDir, logfile.getPath().getName(), fs);
329      }
330      isCorrupted = true;
331    } catch (IOException e) {
332      e = e instanceof RemoteException ? ((RemoteException) e).unwrapRemoteException() : e;
333      throw e;
334    } finally {
335      LOG.debug("Finishing writing output logs and closing down");
336      try {
337        if (null != logFileReader) {
338          logFileReader.close();
339        }
340      } catch (IOException exception) {
341        LOG.warn("Could not close WAL reader", exception);
342      }
343      try {
344        if (outputSinkStarted) {
345          // Set progress_failed to true as the immediate following statement will reset its value
346          // when finishWritingAndClose() throws exception, progress_failed has the right value
347          progress_failed = true;
348          progress_failed = outputSink.finishWritingAndClose() == null;
349        }
350      } finally {
351        String msg =
352            "Processed " + editsCount + " edits across " + outputSink.getNumberOfRecoveredRegions()
353                + " regions; edits skipped=" + editsSkipped + "; log file=" + logPath +
354                ", length=" + logfile.getLen() + // See if length got updated post lease recovery
355                ", corrupted=" + isCorrupted + ", progress failed=" + progress_failed;
356        LOG.info(msg);
357        status.markComplete(msg);
358      }
359    }
360    return !progress_failed;
361  }
362
363  /**
364   * Completes the work done by splitLogFile by archiving logs
365   * <p>
366   * It is invoked by SplitLogManager once it knows that one of the
367   * SplitLogWorkers have completed the splitLogFile() part. If the master
368   * crashes then this function might get called multiple times.
369   * <p>
370   * @param logfile
371   * @param conf
372   * @throws IOException
373   */
374  public static void finishSplitLogFile(String logfile,
375      Configuration conf)  throws IOException {
376    Path rootdir = FSUtils.getWALRootDir(conf);
377    Path oldLogDir = new Path(rootdir, HConstants.HREGION_OLDLOGDIR_NAME);
378    Path logPath;
379    if (FSUtils.isStartingWithPath(rootdir, logfile)) {
380      logPath = new Path(logfile);
381    } else {
382      logPath = new Path(rootdir, logfile);
383    }
384    finishSplitLogFile(rootdir, oldLogDir, logPath, conf);
385  }
386
387  private static void finishSplitLogFile(Path rootdir, Path oldLogDir,
388      Path logPath, Configuration conf) throws IOException {
389    List<Path> processedLogs = new ArrayList<>();
390    List<Path> corruptedLogs = new ArrayList<>();
391    FileSystem fs;
392    fs = rootdir.getFileSystem(conf);
393    if (ZKSplitLog.isCorrupted(rootdir, logPath.getName(), fs)) {
394      corruptedLogs.add(logPath);
395    } else {
396      processedLogs.add(logPath);
397    }
398    archiveLogs(corruptedLogs, processedLogs, oldLogDir, fs, conf);
399    Path stagingDir = ZKSplitLog.getSplitLogDir(rootdir, logPath.getName());
400    fs.delete(stagingDir, true);
401  }
402
403  /**
404   * Moves processed logs to a oldLogDir after successful processing Moves
405   * corrupted logs (any log that couldn't be successfully parsed to corruptDir
406   * (.corrupt) for later investigation
407   *
408   * @param corruptedLogs
409   * @param processedLogs
410   * @param oldLogDir
411   * @param fs
412   * @param conf
413   * @throws IOException
414   */
415  private static void archiveLogs(
416      final List<Path> corruptedLogs,
417      final List<Path> processedLogs, final Path oldLogDir,
418      final FileSystem fs, final Configuration conf) throws IOException {
419    final Path corruptDir = new Path(FSUtils.getWALRootDir(conf), HConstants.CORRUPT_DIR_NAME);
420    if (conf.get("hbase.regionserver.hlog.splitlog.corrupt.dir") != null) {
421      LOG.warn("hbase.regionserver.hlog.splitlog.corrupt.dir is deprecated. Default to {}",
422          corruptDir);
423    }
424    if (!fs.mkdirs(corruptDir)) {
425      LOG.info("Unable to mkdir {}", corruptDir);
426    }
427    fs.mkdirs(oldLogDir);
428
429    // this method can get restarted or called multiple times for archiving
430    // the same log files.
431    for (Path corrupted : corruptedLogs) {
432      Path p = new Path(corruptDir, corrupted.getName());
433      if (fs.exists(corrupted)) {
434        if (!fs.rename(corrupted, p)) {
435          LOG.warn("Unable to move corrupted log {} to {}", corrupted, p);
436        } else {
437          LOG.warn("Moved corrupted log {} to {}", corrupted, p);
438        }
439      }
440    }
441
442    for (Path p : processedLogs) {
443      Path newPath = AbstractFSWAL.getWALArchivePath(oldLogDir, p);
444      if (fs.exists(p)) {
445        if (!FSUtils.renameAndSetModifyTime(fs, p, newPath)) {
446          LOG.warn("Unable to move {} to {}", p, newPath);
447        } else {
448          LOG.info("Archived processed log {} to {}", p, newPath);
449        }
450      }
451    }
452  }
453
454  /**
455   * Path to a file under RECOVERED_EDITS_DIR directory of the region found in
456   * <code>logEntry</code> named for the sequenceid in the passed
457   * <code>logEntry</code>: e.g. /hbase/some_table/2323432434/recovered.edits/2332.
458   * This method also ensures existence of RECOVERED_EDITS_DIR under the region
459   * creating it if necessary.
460   * @param fs
461   * @param logEntry
462   * @param rootDir HBase root dir.
463   * @param fileNameBeingSplit the file being split currently. Used to generate tmp file name.
464   * @return Path to file into which to dump split log edits.
465   * @throws IOException
466   */
467  @SuppressWarnings("deprecation")
468  @VisibleForTesting
469  static Path getRegionSplitEditsPath(final FileSystem fs,
470      final Entry logEntry, final Path rootDir, String fileNameBeingSplit)
471  throws IOException {
472    Path tableDir = FSUtils.getTableDir(rootDir, logEntry.getKey().getTableName());
473    String encodedRegionName = Bytes.toString(logEntry.getKey().getEncodedRegionName());
474    Path regiondir = HRegion.getRegionDir(tableDir, encodedRegionName);
475    Path dir = getRegionDirRecoveredEditsDir(regiondir);
476
477    if (!fs.exists(regiondir)) {
478      LOG.info("This region's directory does not exist: {}."
479          + "It is very likely that it was already split so it is "
480          + "safe to discard those edits.", regiondir);
481      return null;
482    }
483    if (fs.exists(dir) && fs.isFile(dir)) {
484      Path tmp = new Path("/tmp");
485      if (!fs.exists(tmp)) {
486        fs.mkdirs(tmp);
487      }
488      tmp = new Path(tmp,
489        HConstants.RECOVERED_EDITS_DIR + "_" + encodedRegionName);
490      LOG.warn("Found existing old file: {}. It could be some "
491        + "leftover of an old installation. It should be a folder instead. "
492        + "So moving it to {}", dir, tmp);
493      if (!fs.rename(dir, tmp)) {
494        LOG.warn("Failed to sideline old file {}", dir);
495      }
496    }
497
498    if (!fs.exists(dir) && !fs.mkdirs(dir)) {
499      LOG.warn("mkdir failed on {}", dir);
500    }
501    // Append fileBeingSplit to prevent name conflict since we may have duplicate wal entries now.
502    // Append file name ends with RECOVERED_LOG_TMPFILE_SUFFIX to ensure
503    // region's replayRecoveredEdits will not delete it
504    String fileName = formatRecoveredEditsFileName(logEntry.getKey().getSequenceId());
505    fileName = getTmpRecoveredEditsFileName(fileName + "-" + fileNameBeingSplit);
506    return new Path(dir, fileName);
507  }
508
509  private static String getTmpRecoveredEditsFileName(String fileName) {
510    return fileName + RECOVERED_LOG_TMPFILE_SUFFIX;
511  }
512
513  /**
514   * Get the completed recovered edits file path, renaming it to be by last edit
515   * in the file from its first edit. Then we could use the name to skip
516   * recovered edits when doing {@link HRegion#replayRecoveredEditsIfAny}.
517   * @param srcPath
518   * @param maximumEditLogSeqNum
519   * @return dstPath take file's last edit log seq num as the name
520   */
521  private static Path getCompletedRecoveredEditsFilePath(Path srcPath,
522      long maximumEditLogSeqNum) {
523    String fileName = formatRecoveredEditsFileName(maximumEditLogSeqNum);
524    return new Path(srcPath.getParent(), fileName);
525  }
526
527  @VisibleForTesting
528  static String formatRecoveredEditsFileName(final long seqid) {
529    return String.format("%019d", seqid);
530  }
531
532  private static final Pattern EDITFILES_NAME_PATTERN = Pattern.compile("-?[0-9]+");
533  private static final String RECOVERED_LOG_TMPFILE_SUFFIX = ".temp";
534
535  /**
536   * @param regiondir
537   *          This regions directory in the filesystem.
538   * @return The directory that holds recovered edits files for the region
539   *         <code>regiondir</code>
540   */
541  public static Path getRegionDirRecoveredEditsDir(final Path regiondir) {
542    return new Path(regiondir, HConstants.RECOVERED_EDITS_DIR);
543  }
544
545  /**
546   * Check whether there is recovered.edits in the region dir
547   * @param fs FileSystem
548   * @param conf conf
549   * @param regionInfo the region to check
550   * @throws IOException IOException
551   * @return true if recovered.edits exist in the region dir
552   */
553  public static boolean hasRecoveredEdits(final FileSystem fs,
554      final Configuration conf, final RegionInfo regionInfo) throws IOException {
555    // No recovered.edits for non default replica regions
556    if (regionInfo.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
557      return false;
558    }
559    Path rootDir = FSUtils.getRootDir(conf);
560    //Only default replica region can reach here, so we can use regioninfo
561    //directly without converting it to default replica's regioninfo.
562    Path regionDir = HRegion.getRegionDir(rootDir, regionInfo);
563    NavigableSet<Path> files = getSplitEditFilesSorted(fs, regionDir);
564    return files != null && !files.isEmpty();
565  }
566
567
568  /**
569   * Returns sorted set of edit files made by splitter, excluding files
570   * with '.temp' suffix.
571   *
572   * @param fs
573   * @param regiondir
574   * @return Files in passed <code>regiondir</code> as a sorted set.
575   * @throws IOException
576   */
577  public static NavigableSet<Path> getSplitEditFilesSorted(final FileSystem fs,
578      final Path regiondir) throws IOException {
579    NavigableSet<Path> filesSorted = new TreeSet<>();
580    Path editsdir = getRegionDirRecoveredEditsDir(regiondir);
581    if (!fs.exists(editsdir)) {
582      return filesSorted;
583    }
584    FileStatus[] files = FSUtils.listStatus(fs, editsdir, new PathFilter() {
585      @Override
586      public boolean accept(Path p) {
587        boolean result = false;
588        try {
589          // Return files and only files that match the editfile names pattern.
590          // There can be other files in this directory other than edit files.
591          // In particular, on error, we'll move aside the bad edit file giving
592          // it a timestamp suffix. See moveAsideBadEditsFile.
593          Matcher m = EDITFILES_NAME_PATTERN.matcher(p.getName());
594          result = fs.isFile(p) && m.matches();
595          // Skip the file whose name ends with RECOVERED_LOG_TMPFILE_SUFFIX,
596          // because it means splitwal thread is writting this file.
597          if (p.getName().endsWith(RECOVERED_LOG_TMPFILE_SUFFIX)) {
598            result = false;
599          }
600          // Skip SeqId Files
601          if (isSequenceIdFile(p)) {
602            result = false;
603          }
604        } catch (IOException e) {
605          LOG.warn("Failed isFile check on {}", p, e);
606        }
607        return result;
608      }
609    });
610    if (ArrayUtils.isNotEmpty(files)) {
611      Arrays.asList(files).forEach(status -> filesSorted.add(status.getPath()));
612    }
613    return filesSorted;
614  }
615
616  /**
617   * Move aside a bad edits file.
618   *
619   * @param fs
620   * @param edits
621   *          Edits file to move aside.
622   * @return The name of the moved aside file.
623   * @throws IOException
624   */
625  public static Path moveAsideBadEditsFile(final FileSystem fs, final Path edits)
626      throws IOException {
627    Path moveAsideName = new Path(edits.getParent(), edits.getName() + "."
628        + System.currentTimeMillis());
629    if (!fs.rename(edits, moveAsideName)) {
630      LOG.warn("Rename failed from {} to {}", edits, moveAsideName);
631    }
632    return moveAsideName;
633  }
634
635  private static final String SEQUENCE_ID_FILE_SUFFIX = ".seqid";
636  private static final String OLD_SEQUENCE_ID_FILE_SUFFIX = "_seqid";
637  private static final int SEQUENCE_ID_FILE_SUFFIX_LENGTH = SEQUENCE_ID_FILE_SUFFIX.length();
638
639  /**
640   * Is the given file a region open sequence id file.
641   */
642  @VisibleForTesting
643  public static boolean isSequenceIdFile(final Path file) {
644    return file.getName().endsWith(SEQUENCE_ID_FILE_SUFFIX)
645        || file.getName().endsWith(OLD_SEQUENCE_ID_FILE_SUFFIX);
646  }
647
648  private static FileStatus[] getSequenceIdFiles(FileSystem fs, Path regionDir) throws IOException {
649    // TODO: Why are we using a method in here as part of our normal region open where
650    // there is no splitting involved? Fix. St.Ack 01/20/2017.
651    Path editsDir = WALSplitter.getRegionDirRecoveredEditsDir(regionDir);
652    try {
653      FileStatus[] files = fs.listStatus(editsDir, WALSplitter::isSequenceIdFile);
654      return files != null ? files : new FileStatus[0];
655    } catch (FileNotFoundException e) {
656      return new FileStatus[0];
657    }
658  }
659
660  private static long getMaxSequenceId(FileStatus[] files) {
661    long maxSeqId = -1L;
662    for (FileStatus file : files) {
663      String fileName = file.getPath().getName();
664      try {
665        maxSeqId = Math.max(maxSeqId, Long
666          .parseLong(fileName.substring(0, fileName.length() - SEQUENCE_ID_FILE_SUFFIX_LENGTH)));
667      } catch (NumberFormatException ex) {
668        LOG.warn("Invalid SeqId File Name={}", fileName);
669      }
670    }
671    return maxSeqId;
672  }
673
674  /**
675   * Get the max sequence id which is stored in the region directory. -1 if none.
676   */
677  public static long getMaxRegionSequenceId(FileSystem fs, Path regionDir) throws IOException {
678    return getMaxSequenceId(getSequenceIdFiles(fs, regionDir));
679  }
680
681  /**
682   * Create a file with name as region's max sequence id
683   */
684  public static void writeRegionSequenceIdFile(FileSystem fs, Path regionDir, long newMaxSeqId)
685      throws IOException {
686    FileStatus[] files = getSequenceIdFiles(fs, regionDir);
687    long maxSeqId = getMaxSequenceId(files);
688    if (maxSeqId > newMaxSeqId) {
689      throw new IOException("The new max sequence id " + newMaxSeqId +
690        " is less than the old max sequence id " + maxSeqId);
691    }
692    // write a new seqId file
693    Path newSeqIdFile = new Path(WALSplitter.getRegionDirRecoveredEditsDir(regionDir),
694      newMaxSeqId + SEQUENCE_ID_FILE_SUFFIX);
695    if (newMaxSeqId != maxSeqId) {
696      try {
697        if (!fs.createNewFile(newSeqIdFile) && !fs.exists(newSeqIdFile)) {
698          throw new IOException("Failed to create SeqId file:" + newSeqIdFile);
699        }
700        LOG.debug("Wrote file={}, newMaxSeqId={}, maxSeqId={}", newSeqIdFile, newMaxSeqId,
701          maxSeqId);
702      } catch (FileAlreadyExistsException ignored) {
703        // latest hdfs throws this exception. it's all right if newSeqIdFile already exists
704      }
705    }
706    // remove old ones
707    for (FileStatus status : files) {
708      if (!newSeqIdFile.equals(status.getPath())) {
709        fs.delete(status.getPath(), false);
710      }
711    }
712  }
713
714  /**
715   * Create a new {@link Reader} for reading logs to split.
716   *
717   * @param file
718   * @return A new Reader instance, caller should close
719   * @throws IOException
720   * @throws CorruptedLogFileException
721   */
722  protected Reader getReader(FileStatus file, boolean skipErrors, CancelableProgressable reporter)
723      throws IOException, CorruptedLogFileException {
724    Path path = file.getPath();
725    long length = file.getLen();
726    Reader in;
727
728    // Check for possibly empty file. With appends, currently Hadoop reports a
729    // zero length even if the file has been sync'd. Revisit if HDFS-376 or
730    // HDFS-878 is committed.
731    if (length <= 0) {
732      LOG.warn("File {} might be still open, length is 0", path);
733    }
734
735    try {
736      FSUtils.getInstance(fs, conf).recoverFileLease(fs, path, conf, reporter);
737      try {
738        in = getReader(path, reporter);
739      } catch (EOFException e) {
740        if (length <= 0) {
741          // TODO should we ignore an empty, not-last log file if skip.errors
742          // is false? Either way, the caller should decide what to do. E.g.
743          // ignore if this is the last log in sequence.
744          // TODO is this scenario still possible if the log has been
745          // recovered (i.e. closed)
746          LOG.warn("Could not open {} for reading. File is empty", path, e);
747        }
748        // EOFException being ignored
749        return null;
750      }
751    } catch (IOException e) {
752      if (e instanceof FileNotFoundException) {
753        // A wal file may not exist anymore. Nothing can be recovered so move on
754        LOG.warn("File {} does not exist anymore", path, e);
755        return null;
756      }
757      if (!skipErrors || e instanceof InterruptedIOException) {
758        throw e; // Don't mark the file corrupted if interrupted, or not skipErrors
759      }
760      CorruptedLogFileException t =
761        new CorruptedLogFileException("skipErrors=true Could not open wal " +
762            path + " ignoring");
763      t.initCause(e);
764      throw t;
765    }
766    return in;
767  }
768
769  static private Entry getNextLogLine(Reader in, Path path, boolean skipErrors)
770  throws CorruptedLogFileException, IOException {
771    try {
772      return in.next();
773    } catch (EOFException eof) {
774      // truncated files are expected if a RS crashes (see HBASE-2643)
775      LOG.info("EOF from wal {}. Continuing.", path);
776      return null;
777    } catch (IOException e) {
778      // If the IOE resulted from bad file format,
779      // then this problem is idempotent and retrying won't help
780      if (e.getCause() != null &&
781          (e.getCause() instanceof ParseException ||
782           e.getCause() instanceof org.apache.hadoop.fs.ChecksumException)) {
783        LOG.warn("Parse exception from wal {}. Continuing", path, e);
784        return null;
785      }
786      if (!skipErrors) {
787        throw e;
788      }
789      CorruptedLogFileException t =
790        new CorruptedLogFileException("skipErrors=true Ignoring exception" +
791            " while parsing wal " + path + ". Marking as corrupted");
792      t.initCause(e);
793      throw t;
794    }
795  }
796
797  /**
798   * Create a new {@link Writer} for writing log splits.
799   * @return a new Writer instance, caller should close
800   */
801  protected Writer createWriter(Path logfile)
802      throws IOException {
803    return walFactory.createRecoveredEditsWriter(fs, logfile);
804  }
805
806  /**
807   * Create a new {@link Reader} for reading logs to split.
808   * @return new Reader instance, caller should close
809   */
810  protected Reader getReader(Path curLogFile, CancelableProgressable reporter) throws IOException {
811    return walFactory.createReader(fs, curLogFile, reporter);
812  }
813
814  /**
815   * Get current open writers
816   */
817  private int getNumOpenWriters() {
818    int result = 0;
819    if (this.outputSink != null) {
820      result += this.outputSink.getNumOpenWriters();
821    }
822    return result;
823  }
824
825  /**
826   * Contains some methods to control WAL-entries producer / consumer interactions
827   */
828  public static class PipelineController {
829    // If an exception is thrown by one of the other threads, it will be
830    // stored here.
831    AtomicReference<Throwable> thrown = new AtomicReference<>();
832
833    // Wait/notify for when data has been produced by the writer thread,
834    // consumed by the reader thread, or an exception occurred
835    public final Object dataAvailable = new Object();
836
837    void writerThreadError(Throwable t) {
838      thrown.compareAndSet(null, t);
839    }
840
841    /**
842     * Check for errors in the writer threads. If any is found, rethrow it.
843     */
844    void checkForErrors() throws IOException {
845      Throwable thrown = this.thrown.get();
846      if (thrown == null) return;
847      if (thrown instanceof IOException) {
848        throw new IOException(thrown);
849      } else {
850        throw new RuntimeException(thrown);
851      }
852    }
853  }
854
855  /**
856   * Class which accumulates edits and separates them into a buffer per region
857   * while simultaneously accounting RAM usage. Blocks if the RAM usage crosses
858   * a predefined threshold.
859   *
860   * Writer threads then pull region-specific buffers from this class.
861   */
862  public static class EntryBuffers {
863    PipelineController controller;
864
865    Map<byte[], RegionEntryBuffer> buffers = new TreeMap<>(Bytes.BYTES_COMPARATOR);
866
867    /* Track which regions are currently in the middle of writing. We don't allow
868       an IO thread to pick up bytes from a region if we're already writing
869       data for that region in a different IO thread. */
870    Set<byte[]> currentlyWriting = new TreeSet<>(Bytes.BYTES_COMPARATOR);
871
872    long totalBuffered = 0;
873    long maxHeapUsage;
874    boolean splitWriterCreationBounded;
875
876    public EntryBuffers(PipelineController controller, long maxHeapUsage) {
877      this(controller, maxHeapUsage, false);
878    }
879
880    public EntryBuffers(PipelineController controller, long maxHeapUsage,
881        boolean splitWriterCreationBounded){
882      this.controller = controller;
883      this.maxHeapUsage = maxHeapUsage;
884      this.splitWriterCreationBounded = splitWriterCreationBounded;
885    }
886
887    /**
888     * Append a log entry into the corresponding region buffer.
889     * Blocks if the total heap usage has crossed the specified threshold.
890     *
891     * @throws InterruptedException
892     * @throws IOException
893     */
894    public void appendEntry(Entry entry) throws InterruptedException, IOException {
895      WALKey key = entry.getKey();
896
897      RegionEntryBuffer buffer;
898      long incrHeap;
899      synchronized (this) {
900        buffer = buffers.get(key.getEncodedRegionName());
901        if (buffer == null) {
902          buffer = new RegionEntryBuffer(key.getTableName(), key.getEncodedRegionName());
903          buffers.put(key.getEncodedRegionName(), buffer);
904        }
905        incrHeap= buffer.appendEntry(entry);
906      }
907
908      // If we crossed the chunk threshold, wait for more space to be available
909      synchronized (controller.dataAvailable) {
910        totalBuffered += incrHeap;
911        while (totalBuffered > maxHeapUsage && controller.thrown.get() == null) {
912          LOG.debug("Used {} bytes of buffered edits, waiting for IO threads", totalBuffered);
913          controller.dataAvailable.wait(2000);
914        }
915        controller.dataAvailable.notifyAll();
916      }
917      controller.checkForErrors();
918    }
919
920    /**
921     * @return RegionEntryBuffer a buffer of edits to be written.
922     */
923    synchronized RegionEntryBuffer getChunkToWrite() {
924      // The core part of limiting opening writers is it doesn't return chunk only if the
925      // heap size is over maxHeapUsage. Thus it doesn't need to create a writer for each
926      // region during splitting. It will flush all the logs in the buffer after splitting
927      // through a threadpool, which means the number of writers it created is under control.
928      if (splitWriterCreationBounded && totalBuffered < maxHeapUsage) {
929        return null;
930      }
931      long biggestSize = 0;
932      byte[] biggestBufferKey = null;
933
934      for (Map.Entry<byte[], RegionEntryBuffer> entry : buffers.entrySet()) {
935        long size = entry.getValue().heapSize();
936        if (size > biggestSize && (!currentlyWriting.contains(entry.getKey()))) {
937          biggestSize = size;
938          biggestBufferKey = entry.getKey();
939        }
940      }
941      if (biggestBufferKey == null) {
942        return null;
943      }
944
945      RegionEntryBuffer buffer = buffers.remove(biggestBufferKey);
946      currentlyWriting.add(biggestBufferKey);
947      return buffer;
948    }
949
950    void doneWriting(RegionEntryBuffer buffer) {
951      synchronized (this) {
952        boolean removed = currentlyWriting.remove(buffer.encodedRegionName);
953        assert removed;
954      }
955      long size = buffer.heapSize();
956
957      synchronized (controller.dataAvailable) {
958        totalBuffered -= size;
959        // We may unblock writers
960        controller.dataAvailable.notifyAll();
961      }
962    }
963
964    synchronized boolean isRegionCurrentlyWriting(byte[] region) {
965      return currentlyWriting.contains(region);
966    }
967
968    public void waitUntilDrained() {
969      synchronized (controller.dataAvailable) {
970        while (totalBuffered > 0) {
971          try {
972            controller.dataAvailable.wait(2000);
973          } catch (InterruptedException e) {
974            LOG.warn("Got interrupted while waiting for EntryBuffers is drained");
975            Thread.interrupted();
976            break;
977          }
978        }
979      }
980    }
981  }
982
983  /**
984   * A buffer of some number of edits for a given region.
985   * This accumulates edits and also provides a memory optimization in order to
986   * share a single byte array instance for the table and region name.
987   * Also tracks memory usage of the accumulated edits.
988   */
989  public static class RegionEntryBuffer implements HeapSize {
990    long heapInBuffer = 0;
991    List<Entry> entryBuffer;
992    TableName tableName;
993    byte[] encodedRegionName;
994
995    RegionEntryBuffer(TableName tableName, byte[] region) {
996      this.tableName = tableName;
997      this.encodedRegionName = region;
998      this.entryBuffer = new ArrayList<>();
999    }
1000
1001    long appendEntry(Entry entry) {
1002      internify(entry);
1003      entryBuffer.add(entry);
1004      long incrHeap = entry.getEdit().heapSize() +
1005        ClassSize.align(2 * ClassSize.REFERENCE) + // WALKey pointers
1006        0; // TODO linkedlist entry
1007      heapInBuffer += incrHeap;
1008      return incrHeap;
1009    }
1010
1011    private void internify(Entry entry) {
1012      WALKeyImpl k = entry.getKey();
1013      k.internTableName(this.tableName);
1014      k.internEncodedRegionName(this.encodedRegionName);
1015    }
1016
1017    @Override
1018    public long heapSize() {
1019      return heapInBuffer;
1020    }
1021
1022    public byte[] getEncodedRegionName() {
1023      return encodedRegionName;
1024    }
1025
1026    public List<Entry> getEntryBuffer() {
1027      return entryBuffer;
1028    }
1029
1030    public TableName getTableName() {
1031      return tableName;
1032    }
1033  }
1034
1035  public static class WriterThread extends Thread {
1036    private volatile boolean shouldStop = false;
1037    private PipelineController controller;
1038    private EntryBuffers entryBuffers;
1039    private OutputSink outputSink = null;
1040
1041    WriterThread(PipelineController controller, EntryBuffers entryBuffers, OutputSink sink, int i){
1042      super(Thread.currentThread().getName() + "-Writer-" + i);
1043      this.controller = controller;
1044      this.entryBuffers = entryBuffers;
1045      outputSink = sink;
1046    }
1047
1048    @Override
1049    public void run()  {
1050      try {
1051        doRun();
1052      } catch (Throwable t) {
1053        LOG.error("Exiting thread", t);
1054        controller.writerThreadError(t);
1055      }
1056    }
1057
1058    private void doRun() throws IOException {
1059      LOG.trace("Writer thread starting");
1060      while (true) {
1061        RegionEntryBuffer buffer = entryBuffers.getChunkToWrite();
1062        if (buffer == null) {
1063          // No data currently available, wait on some more to show up
1064          synchronized (controller.dataAvailable) {
1065            if (shouldStop && !this.outputSink.flush()) {
1066              return;
1067            }
1068            try {
1069              controller.dataAvailable.wait(500);
1070            } catch (InterruptedException ie) {
1071              if (!shouldStop) {
1072                throw new RuntimeException(ie);
1073              }
1074            }
1075          }
1076          continue;
1077        }
1078
1079        assert buffer != null;
1080        try {
1081          writeBuffer(buffer);
1082        } finally {
1083          entryBuffers.doneWriting(buffer);
1084        }
1085      }
1086    }
1087
1088    private void writeBuffer(RegionEntryBuffer buffer) throws IOException {
1089      outputSink.append(buffer);
1090    }
1091
1092    void finish() {
1093      synchronized (controller.dataAvailable) {
1094        shouldStop = true;
1095        controller.dataAvailable.notifyAll();
1096      }
1097    }
1098  }
1099
1100  /**
1101   * The following class is an abstraction class to provide a common interface to support
1102   * different ways of consuming recovered edits.
1103   */
1104  public static abstract class OutputSink {
1105
1106    protected PipelineController controller;
1107    protected EntryBuffers entryBuffers;
1108
1109    protected ConcurrentHashMap<String, SinkWriter> writers = new ConcurrentHashMap<>();
1110    protected final ConcurrentHashMap<String, Long> regionMaximumEditLogSeqNum =
1111        new ConcurrentHashMap<>();
1112
1113
1114    protected final List<WriterThread> writerThreads = Lists.newArrayList();
1115
1116    /* Set of regions which we've decided should not output edits */
1117    protected final Set<byte[]> blacklistedRegions = Collections
1118        .synchronizedSet(new TreeSet<>(Bytes.BYTES_COMPARATOR));
1119
1120    protected boolean closeAndCleanCompleted = false;
1121
1122    protected boolean writersClosed = false;
1123
1124    protected final int numThreads;
1125
1126    protected CancelableProgressable reporter = null;
1127
1128    protected AtomicLong skippedEdits = new AtomicLong();
1129
1130    protected List<Path> splits = null;
1131
1132    public OutputSink(PipelineController controller, EntryBuffers entryBuffers, int numWriters) {
1133      numThreads = numWriters;
1134      this.controller = controller;
1135      this.entryBuffers = entryBuffers;
1136    }
1137
1138    void setReporter(CancelableProgressable reporter) {
1139      this.reporter = reporter;
1140    }
1141
1142    /**
1143     * Start the threads that will pump data from the entryBuffers to the output files.
1144     */
1145    public synchronized void startWriterThreads() {
1146      for (int i = 0; i < numThreads; i++) {
1147        WriterThread t = new WriterThread(controller, entryBuffers, this, i);
1148        t.start();
1149        writerThreads.add(t);
1150      }
1151    }
1152
1153    /**
1154     *
1155     * Update region's maximum edit log SeqNum.
1156     */
1157    void updateRegionMaximumEditLogSeqNum(Entry entry) {
1158      synchronized (regionMaximumEditLogSeqNum) {
1159        String regionName = Bytes.toString(entry.getKey().getEncodedRegionName());
1160        Long currentMaxSeqNum = regionMaximumEditLogSeqNum.get(regionName);
1161        if (currentMaxSeqNum == null || entry.getKey().getSequenceId() > currentMaxSeqNum) {
1162          regionMaximumEditLogSeqNum.put(regionName, entry.getKey().getSequenceId());
1163        }
1164      }
1165    }
1166
1167    /**
1168     * @return the number of currently opened writers
1169     */
1170    int getNumOpenWriters() {
1171      return this.writers.size();
1172    }
1173
1174    long getSkippedEdits() {
1175      return this.skippedEdits.get();
1176    }
1177
1178    /**
1179     * Wait for writer threads to dump all info to the sink
1180     * @return true when there is no error
1181     * @throws IOException
1182     */
1183    protected boolean finishWriting(boolean interrupt) throws IOException {
1184      LOG.debug("Waiting for split writer threads to finish");
1185      boolean progress_failed = false;
1186      for (WriterThread t : writerThreads) {
1187        t.finish();
1188      }
1189      if (interrupt) {
1190        for (WriterThread t : writerThreads) {
1191          t.interrupt(); // interrupt the writer threads. We are stopping now.
1192        }
1193      }
1194
1195      for (WriterThread t : writerThreads) {
1196        if (!progress_failed && reporter != null && !reporter.progress()) {
1197          progress_failed = true;
1198        }
1199        try {
1200          t.join();
1201        } catch (InterruptedException ie) {
1202          IOException iie = new InterruptedIOException();
1203          iie.initCause(ie);
1204          throw iie;
1205        }
1206      }
1207      controller.checkForErrors();
1208      LOG.info("{} split writers finished; closing.", this.writerThreads.size());
1209      return (!progress_failed);
1210    }
1211
1212    public abstract List<Path> finishWritingAndClose() throws IOException;
1213
1214    /**
1215     * @return a map from encoded region ID to the number of edits written out for that region.
1216     */
1217    public abstract Map<byte[], Long> getOutputCounts();
1218
1219    /**
1220     * @return number of regions we've recovered
1221     */
1222    public abstract int getNumberOfRecoveredRegions();
1223
1224    /**
1225     * @param buffer A WAL Edit Entry
1226     * @throws IOException
1227     */
1228    public abstract void append(RegionEntryBuffer buffer) throws IOException;
1229
1230    /**
1231     * WriterThread call this function to help flush internal remaining edits in buffer before close
1232     * @return true when underlying sink has something to flush
1233     */
1234    public boolean flush() throws IOException {
1235      return false;
1236    }
1237
1238    /**
1239     * Some WALEdit's contain only KV's for account on what happened to a region.
1240     * Not all sinks will want to get all of those edits.
1241     *
1242     * @return Return true if this sink wants to accept this region-level WALEdit.
1243     */
1244    public abstract boolean keepRegionEvent(Entry entry);
1245  }
1246
1247  /**
1248   * Class that manages the output streams from the log splitting process.
1249   */
1250  class LogRecoveredEditsOutputSink extends OutputSink {
1251
1252    public LogRecoveredEditsOutputSink(PipelineController controller, EntryBuffers entryBuffers,
1253        int numWriters) {
1254      // More threads could potentially write faster at the expense
1255      // of causing more disk seeks as the logs are split.
1256      // 3. After a certain setting (probably around 3) the
1257      // process will be bound on the reader in the current
1258      // implementation anyway.
1259      super(controller, entryBuffers, numWriters);
1260    }
1261
1262    /**
1263     * @return null if failed to report progress
1264     * @throws IOException
1265     */
1266    @Override
1267    public List<Path> finishWritingAndClose() throws IOException {
1268      boolean isSuccessful = false;
1269      List<Path> result = null;
1270      try {
1271        isSuccessful = finishWriting(false);
1272      } finally {
1273        result = close();
1274        List<IOException> thrown = closeLogWriters(null);
1275        if (CollectionUtils.isNotEmpty(thrown)) {
1276          throw MultipleIOException.createIOException(thrown);
1277        }
1278      }
1279      if (isSuccessful) {
1280        splits = result;
1281      }
1282      return splits;
1283    }
1284
1285    // delete the one with fewer wal entries
1286    private void deleteOneWithFewerEntries(WriterAndPath wap, Path dst) throws IOException {
1287      long dstMinLogSeqNum = -1L;
1288      try (WAL.Reader reader = walFactory.createReader(fs, dst)) {
1289        WAL.Entry entry = reader.next();
1290        if (entry != null) {
1291          dstMinLogSeqNum = entry.getKey().getSequenceId();
1292        }
1293      } catch (EOFException e) {
1294        LOG.debug("Got EOF when reading first WAL entry from {}, an empty or broken WAL file?",
1295            dst, e);
1296      }
1297      if (wap.minLogSeqNum < dstMinLogSeqNum) {
1298        LOG.warn("Found existing old edits file. It could be the result of a previous failed"
1299            + " split attempt or we have duplicated wal entries. Deleting " + dst + ", length="
1300            + fs.getFileStatus(dst).getLen());
1301        if (!fs.delete(dst, false)) {
1302          LOG.warn("Failed deleting of old {}", dst);
1303          throw new IOException("Failed deleting of old " + dst);
1304        }
1305      } else {
1306        LOG.warn("Found existing old edits file and we have less entries. Deleting " + wap.p
1307            + ", length=" + fs.getFileStatus(wap.p).getLen());
1308        if (!fs.delete(wap.p, false)) {
1309          LOG.warn("Failed deleting of {}", wap.p);
1310          throw new IOException("Failed deleting of " + wap.p);
1311        }
1312      }
1313    }
1314
1315    /**
1316     * Close all of the output streams.
1317     * @return the list of paths written.
1318     */
1319    List<Path> close() throws IOException {
1320      Preconditions.checkState(!closeAndCleanCompleted);
1321
1322      final List<Path> paths = new ArrayList<>();
1323      final List<IOException> thrown = Lists.newArrayList();
1324      ThreadPoolExecutor closeThreadPool = Threads
1325          .getBoundedCachedThreadPool(numThreads, 30L, TimeUnit.SECONDS, new ThreadFactory() {
1326            private int count = 1;
1327
1328            @Override public Thread newThread(Runnable r) {
1329              Thread t = new Thread(r, "split-log-closeStream-" + count++);
1330              return t;
1331            }
1332          });
1333      CompletionService<Void> completionService = new ExecutorCompletionService<>(closeThreadPool);
1334      boolean progress_failed;
1335      try {
1336        progress_failed = executeCloseTask(completionService, thrown, paths);
1337      } catch (InterruptedException e) {
1338        IOException iie = new InterruptedIOException();
1339        iie.initCause(e);
1340        throw iie;
1341      } catch (ExecutionException e) {
1342        throw new IOException(e.getCause());
1343      } finally {
1344        closeThreadPool.shutdownNow();
1345      }
1346      if (!thrown.isEmpty()) {
1347        throw MultipleIOException.createIOException(thrown);
1348      }
1349      writersClosed = true;
1350      closeAndCleanCompleted = true;
1351      if (progress_failed) {
1352        return null;
1353      }
1354      return paths;
1355    }
1356
1357    /**
1358     * @param completionService threadPool to execute the closing tasks
1359     * @param thrown store the exceptions
1360     * @param paths arrayList to store the paths written
1361     * @return if close tasks executed successful
1362     */
1363    boolean executeCloseTask(CompletionService<Void> completionService,
1364        List<IOException> thrown, List<Path> paths)
1365        throws InterruptedException, ExecutionException {
1366      for (final Map.Entry<String, SinkWriter> writersEntry : writers.entrySet()) {
1367        if (LOG.isTraceEnabled()) {
1368          LOG.trace("Submitting close of " + ((WriterAndPath) writersEntry.getValue()).p);
1369        }
1370        completionService.submit(new Callable<Void>() {
1371          @Override public Void call() throws Exception {
1372            WriterAndPath wap = (WriterAndPath) writersEntry.getValue();
1373            Path dst = closeWriter(writersEntry.getKey(), wap, thrown);
1374            paths.add(dst);
1375            return null;
1376          }
1377        });
1378      }
1379      boolean progress_failed = false;
1380      for (int i = 0, n = this.writers.size(); i < n; i++) {
1381        Future<Void> future = completionService.take();
1382        future.get();
1383        if (!progress_failed && reporter != null && !reporter.progress()) {
1384          progress_failed = true;
1385        }
1386      }
1387      return progress_failed;
1388    }
1389
1390    Path closeWriter(String encodedRegionName, WriterAndPath wap,
1391        List<IOException> thrown) throws IOException{
1392      if (LOG.isTraceEnabled()) {
1393        LOG.trace("Closing " + wap.p);
1394      }
1395      try {
1396        wap.w.close();
1397      } catch (IOException ioe) {
1398        LOG.error("Couldn't close log at " + wap.p, ioe);
1399        thrown.add(ioe);
1400        return null;
1401      }
1402      if (LOG.isDebugEnabled()) {
1403        LOG.debug("Closed wap " + wap.p + " (wrote " + wap.editsWritten
1404            + " edits, skipped " + wap.editsSkipped + " edits in "
1405            + (wap.nanosSpent / 1000 / 1000) + "ms");
1406      }
1407      if (wap.editsWritten == 0) {
1408        // just remove the empty recovered.edits file
1409        if (fs.exists(wap.p) && !fs.delete(wap.p, false)) {
1410          LOG.warn("Failed deleting empty " + wap.p);
1411          throw new IOException("Failed deleting empty  " + wap.p);
1412        }
1413        return null;
1414      }
1415
1416      Path dst = getCompletedRecoveredEditsFilePath(wap.p,
1417          regionMaximumEditLogSeqNum.get(encodedRegionName));
1418      try {
1419        if (!dst.equals(wap.p) && fs.exists(dst)) {
1420          deleteOneWithFewerEntries(wap, dst);
1421        }
1422        // Skip the unit tests which create a splitter that reads and
1423        // writes the data without touching disk.
1424        // TestHLogSplit#testThreading is an example.
1425        if (fs.exists(wap.p)) {
1426          if (!fs.rename(wap.p, dst)) {
1427            throw new IOException("Failed renaming " + wap.p + " to " + dst);
1428          }
1429          LOG.info("Rename " + wap.p + " to " + dst);
1430        }
1431      } catch (IOException ioe) {
1432        LOG.error("Couldn't rename " + wap.p + " to " + dst, ioe);
1433        thrown.add(ioe);
1434        return null;
1435      }
1436      return dst;
1437    }
1438
1439    private List<IOException> closeLogWriters(List<IOException> thrown) throws IOException {
1440      if (writersClosed) {
1441        return thrown;
1442      }
1443      if (thrown == null) {
1444        thrown = Lists.newArrayList();
1445      }
1446      try {
1447        for (WriterThread t : writerThreads) {
1448          while (t.isAlive()) {
1449            t.shouldStop = true;
1450            t.interrupt();
1451            try {
1452              t.join(10);
1453            } catch (InterruptedException e) {
1454              IOException iie = new InterruptedIOException();
1455              iie.initCause(e);
1456              throw iie;
1457            }
1458          }
1459        }
1460      } finally {
1461        WriterAndPath wap = null;
1462        for (SinkWriter tmpWAP : writers.values()) {
1463          try {
1464            wap = (WriterAndPath) tmpWAP;
1465            wap.w.close();
1466          } catch (IOException ioe) {
1467            LOG.error("Couldn't close log at " + wap.p, ioe);
1468            thrown.add(ioe);
1469            continue;
1470          }
1471          LOG.info(
1472              "Closed log " + wap.p + " (wrote " + wap.editsWritten + " edits in " + (wap.nanosSpent
1473                  / 1000 / 1000) + "ms)");
1474        }
1475        writersClosed = true;
1476      }
1477
1478      return thrown;
1479    }
1480
1481    /**
1482     * Get a writer and path for a log starting at the given entry. This function is threadsafe so
1483     * long as multiple threads are always acting on different regions.
1484     * @return null if this region shouldn't output any logs
1485     */
1486    WriterAndPath getWriterAndPath(Entry entry, boolean reusable) throws IOException {
1487      byte region[] = entry.getKey().getEncodedRegionName();
1488      String regionName = Bytes.toString(region);
1489      WriterAndPath ret = (WriterAndPath) writers.get(regionName);
1490      if (ret != null) {
1491        return ret;
1492      }
1493      // If we already decided that this region doesn't get any output
1494      // we don't need to check again.
1495      if (blacklistedRegions.contains(region)) {
1496        return null;
1497      }
1498      ret = createWAP(region, entry, rootDir);
1499      if (ret == null) {
1500        blacklistedRegions.add(region);
1501        return null;
1502      }
1503      if(reusable) {
1504        writers.put(regionName, ret);
1505      }
1506      return ret;
1507    }
1508
1509    /**
1510     * @return a path with a write for that path. caller should close.
1511     */
1512    WriterAndPath createWAP(byte[] region, Entry entry, Path rootdir) throws IOException {
1513      Path regionedits = getRegionSplitEditsPath(fs, entry, rootdir, fileBeingSplit.getPath().getName());
1514      if (regionedits == null) {
1515        return null;
1516      }
1517      if (fs.exists(regionedits)) {
1518        LOG.warn("Found old edits file. It could be the "
1519            + "result of a previous failed split attempt. Deleting " + regionedits + ", length="
1520            + fs.getFileStatus(regionedits).getLen());
1521        if (!fs.delete(regionedits, false)) {
1522          LOG.warn("Failed delete of old {}", regionedits);
1523        }
1524      }
1525      Writer w = createWriter(regionedits);
1526      LOG.debug("Creating writer path={}", regionedits);
1527      return new WriterAndPath(regionedits, w, entry.getKey().getSequenceId());
1528    }
1529
1530    void filterCellByStore(Entry logEntry) {
1531      Map<byte[], Long> maxSeqIdInStores =
1532          regionMaxSeqIdInStores.get(Bytes.toString(logEntry.getKey().getEncodedRegionName()));
1533      if (MapUtils.isEmpty(maxSeqIdInStores)) {
1534        return;
1535      }
1536      // Create the array list for the cells that aren't filtered.
1537      // We make the assumption that most cells will be kept.
1538      ArrayList<Cell> keptCells = new ArrayList<>(logEntry.getEdit().getCells().size());
1539      for (Cell cell : logEntry.getEdit().getCells()) {
1540        if (CellUtil.matchingFamily(cell, WALEdit.METAFAMILY)) {
1541          keptCells.add(cell);
1542        } else {
1543          byte[] family = CellUtil.cloneFamily(cell);
1544          Long maxSeqId = maxSeqIdInStores.get(family);
1545          // Do not skip cell even if maxSeqId is null. Maybe we are in a rolling upgrade,
1546          // or the master was crashed before and we can not get the information.
1547          if (maxSeqId == null || maxSeqId.longValue() < logEntry.getKey().getSequenceId()) {
1548            keptCells.add(cell);
1549          }
1550        }
1551      }
1552
1553      // Anything in the keptCells array list is still live.
1554      // So rather than removing the cells from the array list
1555      // which would be an O(n^2) operation, we just replace the list
1556      logEntry.getEdit().setCells(keptCells);
1557    }
1558
1559    @Override
1560    public void append(RegionEntryBuffer buffer) throws IOException {
1561      appendBuffer(buffer, true);
1562    }
1563
1564    WriterAndPath appendBuffer(RegionEntryBuffer buffer, boolean reusable) throws IOException{
1565      List<Entry> entries = buffer.entryBuffer;
1566      if (entries.isEmpty()) {
1567        LOG.warn("got an empty buffer, skipping");
1568        return null;
1569      }
1570
1571      WriterAndPath wap = null;
1572
1573      long startTime = System.nanoTime();
1574      try {
1575        int editsCount = 0;
1576
1577        for (Entry logEntry : entries) {
1578          if (wap == null) {
1579            wap = getWriterAndPath(logEntry, reusable);
1580            if (wap == null) {
1581              if (LOG.isTraceEnabled()) {
1582                // This log spews the full edit. Can be massive in the log. Enable only debugging
1583                // WAL lost edit issues.
1584                LOG.trace("getWriterAndPath decided we don't need to write edits for {}", logEntry);
1585              }
1586              return null;
1587            }
1588          }
1589          filterCellByStore(logEntry);
1590          if (!logEntry.getEdit().isEmpty()) {
1591            wap.w.append(logEntry);
1592            this.updateRegionMaximumEditLogSeqNum(logEntry);
1593            editsCount++;
1594          } else {
1595            wap.incrementSkippedEdits(1);
1596          }
1597        }
1598        // Pass along summary statistics
1599        wap.incrementEdits(editsCount);
1600        wap.incrementNanoTime(System.nanoTime() - startTime);
1601      } catch (IOException e) {
1602          e = e instanceof RemoteException ?
1603                  ((RemoteException)e).unwrapRemoteException() : e;
1604        LOG.error(HBaseMarkers.FATAL, "Got while writing log entry to log", e);
1605        throw e;
1606      }
1607      return wap;
1608    }
1609
1610    @Override
1611    public boolean keepRegionEvent(Entry entry) {
1612      ArrayList<Cell> cells = entry.getEdit().getCells();
1613      for (Cell cell : cells) {
1614        if (WALEdit.isCompactionMarker(cell)) {
1615          return true;
1616        }
1617      }
1618      return false;
1619    }
1620
1621    /**
1622     * @return a map from encoded region ID to the number of edits written out for that region.
1623     */
1624    @Override
1625    public Map<byte[], Long> getOutputCounts() {
1626      TreeMap<byte[], Long> ret = new TreeMap<>(Bytes.BYTES_COMPARATOR);
1627      for (Map.Entry<String, SinkWriter> entry : writers.entrySet()) {
1628        ret.put(Bytes.toBytes(entry.getKey()), entry.getValue().editsWritten);
1629      }
1630      return ret;
1631    }
1632
1633    @Override
1634    public int getNumberOfRecoveredRegions() {
1635      return writers.size();
1636    }
1637  }
1638
1639  /**
1640   *
1641   */
1642  class BoundedLogWriterCreationOutputSink extends LogRecoveredEditsOutputSink {
1643
1644    private ConcurrentHashMap<String, Long> regionRecoverStatMap = new ConcurrentHashMap<>();
1645
1646    public BoundedLogWriterCreationOutputSink(PipelineController controller,
1647        EntryBuffers entryBuffers, int numWriters) {
1648      super(controller, entryBuffers, numWriters);
1649    }
1650
1651    @Override
1652    public List<Path> finishWritingAndClose() throws IOException {
1653      boolean isSuccessful;
1654      List<Path> result;
1655      try {
1656        isSuccessful = finishWriting(false);
1657      } finally {
1658        result = close();
1659      }
1660      if (isSuccessful) {
1661        splits = result;
1662      }
1663      return splits;
1664    }
1665
1666    @Override
1667    boolean executeCloseTask(CompletionService<Void> completionService,
1668        List<IOException> thrown, List<Path> paths)
1669        throws InterruptedException, ExecutionException {
1670      for (final Map.Entry<byte[], RegionEntryBuffer> buffer : entryBuffers.buffers.entrySet()) {
1671        LOG.info("Submitting writeThenClose of {}",
1672            Arrays.toString(buffer.getValue().encodedRegionName));
1673        completionService.submit(new Callable<Void>() {
1674          @Override
1675          public Void call() throws Exception {
1676            Path dst = writeThenClose(buffer.getValue());
1677            paths.add(dst);
1678            return null;
1679          }
1680        });
1681      }
1682      boolean progress_failed = false;
1683      for (int i = 0, n = entryBuffers.buffers.size(); i < n; i++) {
1684        Future<Void> future = completionService.take();
1685        future.get();
1686        if (!progress_failed && reporter != null && !reporter.progress()) {
1687          progress_failed = true;
1688        }
1689      }
1690
1691      return progress_failed;
1692    }
1693
1694    /**
1695     * since the splitting process may create multiple output files, we need a map
1696     * regionRecoverStatMap to track the output count of each region.
1697     * @return a map from encoded region ID to the number of edits written out for that region.
1698     */
1699    @Override
1700    public Map<byte[], Long> getOutputCounts() {
1701      Map<byte[], Long> regionRecoverStatMapResult = new HashMap<>();
1702      for(Map.Entry<String, Long> entry: regionRecoverStatMap.entrySet()){
1703        regionRecoverStatMapResult.put(Bytes.toBytes(entry.getKey()), entry.getValue());
1704      }
1705      return regionRecoverStatMapResult;
1706    }
1707
1708    /**
1709     * @return the number of recovered regions
1710     */
1711    @Override
1712    public int getNumberOfRecoveredRegions() {
1713      return regionRecoverStatMap.size();
1714    }
1715
1716    /**
1717     * Append the buffer to a new recovered edits file, then close it after all done
1718     * @param buffer contain all entries of a certain region
1719     * @throws IOException when closeWriter failed
1720     */
1721    @Override
1722    public void append(RegionEntryBuffer buffer) throws IOException {
1723      writeThenClose(buffer);
1724    }
1725
1726    private Path writeThenClose(RegionEntryBuffer buffer) throws IOException {
1727      WriterAndPath wap = appendBuffer(buffer, false);
1728      if(wap != null) {
1729        String encodedRegionName = Bytes.toString(buffer.encodedRegionName);
1730        Long value = regionRecoverStatMap.putIfAbsent(encodedRegionName, wap.editsWritten);
1731        if (value != null) {
1732          Long newValue = regionRecoverStatMap.get(encodedRegionName) + wap.editsWritten;
1733          regionRecoverStatMap.put(encodedRegionName, newValue);
1734        }
1735      }
1736
1737      Path dst = null;
1738      List<IOException> thrown = new ArrayList<>();
1739      if(wap != null){
1740        dst = closeWriter(Bytes.toString(buffer.encodedRegionName), wap, thrown);
1741      }
1742      if (!thrown.isEmpty()) {
1743        throw MultipleIOException.createIOException(thrown);
1744      }
1745      return dst;
1746    }
1747  }
1748
1749  /**
1750   * Class wraps the actual writer which writes data out and related statistics
1751   */
1752  public abstract static class SinkWriter {
1753    /* Count of edits written to this path */
1754    long editsWritten = 0;
1755    /* Count of edits skipped to this path */
1756    long editsSkipped = 0;
1757    /* Number of nanos spent writing to this log */
1758    long nanosSpent = 0;
1759
1760    void incrementEdits(int edits) {
1761      editsWritten += edits;
1762    }
1763
1764    void incrementSkippedEdits(int skipped) {
1765      editsSkipped += skipped;
1766    }
1767
1768    void incrementNanoTime(long nanos) {
1769      nanosSpent += nanos;
1770    }
1771  }
1772
1773  /**
1774   * Private data structure that wraps a Writer and its Path, also collecting statistics about the
1775   * data written to this output.
1776   */
1777  private final static class WriterAndPath extends SinkWriter {
1778    final Path p;
1779    final Writer w;
1780    final long minLogSeqNum;
1781
1782    WriterAndPath(final Path p, final Writer w, final long minLogSeqNum) {
1783      this.p = p;
1784      this.w = w;
1785      this.minLogSeqNum = minLogSeqNum;
1786    }
1787  }
1788
1789  static class CorruptedLogFileException extends Exception {
1790    private static final long serialVersionUID = 1L;
1791
1792    CorruptedLogFileException(String s) {
1793      super(s);
1794    }
1795  }
1796
1797  /** A struct used by getMutationsFromWALEntry */
1798  public static class MutationReplay implements Comparable<MutationReplay> {
1799    public MutationReplay(MutationType type, Mutation mutation, long nonceGroup, long nonce) {
1800      this.type = type;
1801      this.mutation = mutation;
1802      if(this.mutation.getDurability() != Durability.SKIP_WAL) {
1803        // using ASYNC_WAL for relay
1804        this.mutation.setDurability(Durability.ASYNC_WAL);
1805      }
1806      this.nonceGroup = nonceGroup;
1807      this.nonce = nonce;
1808    }
1809
1810    public final MutationType type;
1811    public final Mutation mutation;
1812    public final long nonceGroup;
1813    public final long nonce;
1814
1815    @Override
1816    public int compareTo(final MutationReplay d) {
1817      return this.mutation.compareTo(d.mutation);
1818    }
1819
1820    @Override
1821    public boolean equals(Object obj) {
1822      if(!(obj instanceof MutationReplay)) {
1823        return false;
1824      } else {
1825        return this.compareTo((MutationReplay)obj) == 0;
1826      }
1827    }
1828
1829    @Override
1830    public int hashCode() {
1831      return this.mutation.hashCode();
1832    }
1833  }
1834
1835  /**
1836   * This function is used to construct mutations from a WALEntry. It also
1837   * reconstructs WALKey &amp; WALEdit from the passed in WALEntry
1838   * @param entry
1839   * @param cells
1840   * @param logEntry pair of WALKey and WALEdit instance stores WALKey and WALEdit instances
1841   *          extracted from the passed in WALEntry.
1842   * @return list of Pair&lt;MutationType, Mutation&gt; to be replayed
1843   * @throws IOException
1844   */
1845  public static List<MutationReplay> getMutationsFromWALEntry(WALEntry entry, CellScanner cells,
1846      Pair<WALKey, WALEdit> logEntry, Durability durability) throws IOException {
1847    if (entry == null) {
1848      // return an empty array
1849      return Collections.emptyList();
1850    }
1851
1852    long replaySeqId = (entry.getKey().hasOrigSequenceNumber()) ?
1853      entry.getKey().getOrigSequenceNumber() : entry.getKey().getLogSequenceNumber();
1854    int count = entry.getAssociatedCellCount();
1855    List<MutationReplay> mutations = new ArrayList<>();
1856    Cell previousCell = null;
1857    Mutation m = null;
1858    WALKeyImpl key = null;
1859    WALEdit val = null;
1860    if (logEntry != null) {
1861      val = new WALEdit();
1862    }
1863
1864    for (int i = 0; i < count; i++) {
1865      // Throw index out of bounds if our cell count is off
1866      if (!cells.advance()) {
1867        throw new ArrayIndexOutOfBoundsException("Expected=" + count + ", index=" + i);
1868      }
1869      Cell cell = cells.current();
1870      if (val != null) val.add(cell);
1871
1872      boolean isNewRowOrType =
1873          previousCell == null || previousCell.getTypeByte() != cell.getTypeByte()
1874              || !CellUtil.matchingRows(previousCell, cell);
1875      if (isNewRowOrType) {
1876        // Create new mutation
1877        if (CellUtil.isDelete(cell)) {
1878          m = new Delete(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
1879          // Deletes don't have nonces.
1880          mutations.add(new MutationReplay(
1881              MutationType.DELETE, m, HConstants.NO_NONCE, HConstants.NO_NONCE));
1882        } else {
1883          m = new Put(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
1884          // Puts might come from increment or append, thus we need nonces.
1885          long nonceGroup = entry.getKey().hasNonceGroup()
1886              ? entry.getKey().getNonceGroup() : HConstants.NO_NONCE;
1887          long nonce = entry.getKey().hasNonce() ? entry.getKey().getNonce() : HConstants.NO_NONCE;
1888          mutations.add(new MutationReplay(MutationType.PUT, m, nonceGroup, nonce));
1889        }
1890      }
1891      if (CellUtil.isDelete(cell)) {
1892        ((Delete) m).add(cell);
1893      } else {
1894        ((Put) m).add(cell);
1895      }
1896      m.setDurability(durability);
1897      previousCell = cell;
1898    }
1899
1900    // reconstruct WALKey
1901    if (logEntry != null) {
1902      org.apache.hadoop.hbase.shaded.protobuf.generated.WALProtos.WALKey walKeyProto =
1903          entry.getKey();
1904      List<UUID> clusterIds = new ArrayList<>(walKeyProto.getClusterIdsCount());
1905      for (HBaseProtos.UUID uuid : entry.getKey().getClusterIdsList()) {
1906        clusterIds.add(new UUID(uuid.getMostSigBits(), uuid.getLeastSigBits()));
1907      }
1908      key = new WALKeyImpl(walKeyProto.getEncodedRegionName().toByteArray(), TableName.valueOf(
1909              walKeyProto.getTableName().toByteArray()), replaySeqId, walKeyProto.getWriteTime(),
1910              clusterIds, walKeyProto.getNonceGroup(), walKeyProto.getNonce(), null);
1911      logEntry.setFirst(key);
1912      logEntry.setSecond(val);
1913    }
1914
1915    return mutations;
1916  }
1917}