View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.wal;
20  
21  import java.io.EOFException;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InterruptedIOException;
25  import java.text.ParseException;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.NavigableSet;
34  import java.util.Set;
35  import java.util.TreeMap;
36  import java.util.TreeSet;
37  import java.util.UUID;
38  import java.util.concurrent.Callable;
39  import java.util.concurrent.CompletionService;
40  import java.util.concurrent.ConcurrentHashMap;
41  import java.util.concurrent.ExecutionException;
42  import java.util.concurrent.ExecutorCompletionService;
43  import java.util.concurrent.Future;
44  import java.util.concurrent.ThreadFactory;
45  import java.util.concurrent.ThreadPoolExecutor;
46  import java.util.concurrent.TimeUnit;
47  import java.util.concurrent.atomic.AtomicBoolean;
48  import java.util.concurrent.atomic.AtomicLong;
49  import java.util.concurrent.atomic.AtomicReference;
50  import java.util.regex.Matcher;
51  import java.util.regex.Pattern;
52  
53  import org.apache.commons.logging.Log;
54  import org.apache.commons.logging.LogFactory;
55  import org.apache.hadoop.conf.Configuration;
56  import org.apache.hadoop.fs.FileAlreadyExistsException;
57  import org.apache.hadoop.fs.FileStatus;
58  import org.apache.hadoop.fs.FileSystem;
59  import org.apache.hadoop.fs.Path;
60  import org.apache.hadoop.fs.PathFilter;
61  import org.apache.hadoop.hbase.Cell;
62  import org.apache.hadoop.hbase.CellScanner;
63  import org.apache.hadoop.hbase.CellUtil;
64  import org.apache.hadoop.hbase.CoordinatedStateException;
65  import org.apache.hadoop.hbase.CoordinatedStateManager;
66  import org.apache.hadoop.hbase.HBaseConfiguration;
67  import org.apache.hadoop.hbase.HConstants;
68  import org.apache.hadoop.hbase.HRegionInfo;
69  import org.apache.hadoop.hbase.HRegionLocation;
70  import org.apache.hadoop.hbase.RemoteExceptionHandler;
71  import org.apache.hadoop.hbase.ServerName;
72  import org.apache.hadoop.hbase.TableName;
73  import org.apache.hadoop.hbase.TableNotFoundException;
74  import org.apache.hadoop.hbase.TableStateManager;
75  import org.apache.hadoop.hbase.classification.InterfaceAudience;
76  import org.apache.hadoop.hbase.client.ConnectionUtils;
77  import org.apache.hadoop.hbase.client.Delete;
78  import org.apache.hadoop.hbase.client.Durability;
79  import org.apache.hadoop.hbase.client.HConnection;
80  import org.apache.hadoop.hbase.client.HConnectionManager;
81  import org.apache.hadoop.hbase.client.Mutation;
82  import org.apache.hadoop.hbase.client.Put;
83  import org.apache.hadoop.hbase.coordination.BaseCoordinatedStateManager;
84  import org.apache.hadoop.hbase.coordination.ZKSplitLogManagerCoordination;
85  import org.apache.hadoop.hbase.exceptions.RegionOpeningException;
86  import org.apache.hadoop.hbase.io.HeapSize;
87  import org.apache.hadoop.hbase.master.SplitLogManager;
88  import org.apache.hadoop.hbase.monitoring.MonitoredTask;
89  import org.apache.hadoop.hbase.monitoring.TaskMonitor;
90  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
91  import org.apache.hadoop.hbase.protobuf.RequestConverter;
92  import org.apache.hadoop.hbase.protobuf.generated.AdminProtos.AdminService.BlockingInterface;
93  import org.apache.hadoop.hbase.protobuf.generated.AdminProtos.GetRegionInfoRequest;
94  import org.apache.hadoop.hbase.protobuf.generated.AdminProtos.GetRegionInfoResponse;
95  import org.apache.hadoop.hbase.protobuf.generated.AdminProtos.WALEntry;
96  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutationProto.MutationType;
97  import org.apache.hadoop.hbase.protobuf.generated.ClusterStatusProtos.RegionStoreSequenceIds;
98  import org.apache.hadoop.hbase.protobuf.generated.ClusterStatusProtos.StoreSequenceId;
99  import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos;
100 import org.apache.hadoop.hbase.protobuf.generated.WALProtos.CompactionDescriptor;
101 import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos;
102 import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos.SplitLogTask.RecoveryMode;
103 import org.apache.hadoop.hbase.regionserver.HRegion;
104 import org.apache.hadoop.hbase.regionserver.LastSequenceId;
105 // imports for things that haven't moved from regionserver.wal yet.
106 import org.apache.hadoop.hbase.regionserver.wal.FSHLog;
107 import org.apache.hadoop.hbase.regionserver.wal.HLogKey;
108 import org.apache.hadoop.hbase.regionserver.wal.WALCellCodec;
109 import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
110 import org.apache.hadoop.hbase.regionserver.wal.WALEditsReplaySink;
111 import org.apache.hadoop.hbase.util.Bytes;
112 import org.apache.hadoop.hbase.util.CancelableProgressable;
113 import org.apache.hadoop.hbase.util.ClassSize;
114 import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
115 import org.apache.hadoop.hbase.util.FSUtils;
116 import org.apache.hadoop.hbase.util.Pair;
117 import org.apache.hadoop.hbase.util.Threads;
118 import org.apache.hadoop.hbase.wal.WAL.Entry;
119 import org.apache.hadoop.hbase.wal.WAL.Reader;
120 import org.apache.hadoop.hbase.wal.WALProvider.Writer;
121 import org.apache.hadoop.hbase.zookeeper.ZKSplitLog;
122 import org.apache.hadoop.io.MultipleIOException;
123 
124 import com.google.common.annotations.VisibleForTesting;
125 import com.google.common.base.Preconditions;
126 import com.google.common.collect.Lists;
127 import com.google.protobuf.ServiceException;
128 import com.google.protobuf.TextFormat;
129 
130 /**
131  * This class is responsible for splitting up a bunch of regionserver commit log
132  * files that are no longer being written to, into new files, one per region for
133  * region to replay on startup. Delete the old log files when finished.
134  */
135 @InterfaceAudience.Private
136 public class WALSplitter {
137   private static final Log LOG = LogFactory.getLog(WALSplitter.class);
138 
139   /** By default we retry errors in splitting, rather than skipping. */
140   public static final boolean SPLIT_SKIP_ERRORS_DEFAULT = false;
141 
142   // Parameters for split process
143   protected final Path walDir;
144   protected final FileSystem walFS;
145   protected final Configuration conf;
146 
147   // Major subcomponents of the split process.
148   // These are separated into inner classes to make testing easier.
149   PipelineController controller;
150   OutputSink outputSink;
151   EntryBuffers entryBuffers;
152 
153   private Set<TableName> disablingOrDisabledTables =
154       new HashSet<TableName>();
155   private BaseCoordinatedStateManager csm;
156   private final WALFactory walFactory;
157 
158   private MonitoredTask status;
159 
160   // For checking the latest flushed sequence id
161   protected final LastSequenceId sequenceIdChecker;
162 
163   protected boolean distributedLogReplay;
164 
165   private final boolean splitWriterCreationBounded;
166 
167 
168   // Map encodedRegionName -> lastFlushedSequenceId
169   protected Map<String, Long> lastFlushedSequenceIds = new ConcurrentHashMap<String, Long>();
170 
171   // Map encodedRegionName -> maxSeqIdInStores
172   protected Map<String, Map<byte[], Long>> regionMaxSeqIdInStores =
173       new ConcurrentHashMap<String, Map<byte[], Long>>();
174 
175   // Failed region server that the wal file being split belongs to
176   protected String failedServerName = "";
177 
178   // Number of writer threads
179   private final int numWriterThreads;
180 
181   // Min batch size when replay WAL edits
182   private final int minBatchSize;
183 
184   // the file being split currently
185   private FileStatus fileBeingSplit;
186 
187   public final static String SPLIT_WRITER_CREATION_BOUNDED = "hbase.split.writer.creation.bounded";
188 
189   @VisibleForTesting
190   WALSplitter(final WALFactory factory, Configuration conf, Path walDir,
191       FileSystem walFS, LastSequenceId idChecker,
192       CoordinatedStateManager csm, RecoveryMode mode) {
193     this.conf = HBaseConfiguration.create(conf);
194     String codecClassName = conf
195         .get(WALCellCodec.WAL_CELL_CODEC_CLASS_KEY, WALCellCodec.class.getName());
196     this.conf.set(HConstants.RPC_CODEC_CONF_KEY, codecClassName);
197     this.walDir = walDir;
198     this.walFS = walFS;
199     this.sequenceIdChecker = idChecker;
200     this.csm = (BaseCoordinatedStateManager)csm;
201     this.walFactory = factory;
202     this.controller = new PipelineController();
203     this.splitWriterCreationBounded = conf.getBoolean(SPLIT_WRITER_CREATION_BOUNDED, false);
204     entryBuffers = new EntryBuffers(controller,
205         this.conf.getInt("hbase.regionserver.hlog.splitlog.buffersize",
206             128*1024*1024), splitWriterCreationBounded);
207 
208     // a larger minBatchSize may slow down recovery because replay writer has to wait for
209     // enough edits before replaying them
210     this.minBatchSize = this.conf.getInt("hbase.regionserver.wal.logreplay.batch.size", 64);
211     this.distributedLogReplay = (RecoveryMode.LOG_REPLAY == mode);
212 
213     this.numWriterThreads = this.conf.getInt("hbase.regionserver.hlog.splitlog.writer.threads", 3);
214     if (csm != null && this.distributedLogReplay) {
215       outputSink = new LogReplayOutputSink(controller, entryBuffers, numWriterThreads);
216     } else {
217       if (this.distributedLogReplay) {
218         LOG.info("ZooKeeperWatcher is passed in as NULL so disable distrubitedLogRepaly.");
219       }
220       this.distributedLogReplay = false;
221       if(splitWriterCreationBounded){
222         outputSink = new BoundedLogWriterCreationOutputSink(controller,
223             entryBuffers, numWriterThreads);
224       }else {
225         outputSink = new LogRecoveredEditsOutputSink(controller, entryBuffers, numWriterThreads);
226       }
227     }
228 
229   }
230 
231   /**
232    * Splits a WAL file into region's recovered-edits directory.
233    * This is the main entry point for distributed log splitting from SplitLogWorker.
234    * <p>
235    * If the log file has N regions then N recovered.edits files will be produced.
236    * <p>
237    * @param rootDir
238    * @param logfile
239    * @param walFS FileSystem to use for WAL reading and splitting
240    * @param conf
241    * @param reporter
242    * @param idChecker
243    * @param cp coordination state manager
244    * @return false if it is interrupted by the progress-able.
245    * @throws IOException
246    */
247   public static boolean splitLogFile(Path walDir, FileStatus logfile, FileSystem walFS,
248       Configuration conf, CancelableProgressable reporter, LastSequenceId idChecker,
249       CoordinatedStateManager cp, RecoveryMode mode, final WALFactory factory) throws IOException {
250     WALSplitter s = new WALSplitter(factory, conf, walDir, walFS, idChecker, cp, mode);
251     return s.splitLogFile(logfile, reporter);
252   }
253 
254   // A wrapper to split one log folder using the method used by distributed
255   // log splitting. Used by tools and unit tests. It should be package private.
256   // It is public only because UpgradeTo96 and TestWALObserver are in different packages,
257   // which uses this method to do log splitting.
258   public static List<Path> split(Path walRootDir, Path logDir, Path oldLogDir,
259       FileSystem walFs, Configuration conf, final WALFactory factory) throws IOException {
260     final FileStatus[] logfiles = SplitLogManager.getFileList(conf,
261         Collections.singletonList(logDir), null);
262     List<Path> splits = new ArrayList<Path>();
263     if (logfiles != null && logfiles.length > 0) {
264       for (FileStatus logfile: logfiles) {
265         WALSplitter s = new WALSplitter(factory, conf, walRootDir, walFs, null, null,
266             RecoveryMode.LOG_SPLITTING);
267         if (s.splitLogFile(logfile, null)) {
268           finishSplitLogFile(walRootDir, oldLogDir, logfile.getPath(), conf);
269           if (s.outputSink.splits != null) {
270             splits.addAll(s.outputSink.splits);
271           }
272         }
273       }
274     }
275     if (!walFs.delete(logDir, true)) {
276       throw new IOException("Unable to delete src dir: " + logDir);
277     }
278     return splits;
279   }
280 
281   /**
282    * log splitting implementation, splits one log file.
283    * @param logfile should be an actual log file.
284    */
285   @VisibleForTesting
286   boolean splitLogFile(FileStatus logfile, CancelableProgressable reporter) throws IOException {
287     Preconditions.checkState(status == null);
288     Preconditions.checkArgument(logfile.isFile(),
289         "passed in file status is for something other than a regular file.");
290     boolean isCorrupted = false;
291     boolean skipErrors = conf.getBoolean("hbase.hlog.split.skip.errors",
292       SPLIT_SKIP_ERRORS_DEFAULT);
293     int interval = conf.getInt("hbase.splitlog.report.interval.loglines", 1024);
294     Path logPath = logfile.getPath();
295     boolean outputSinkStarted = false;
296     boolean progress_failed = false;
297     int editsCount = 0;
298     int editsSkipped = 0;
299 
300     status =
301         TaskMonitor.get().createStatus(
302           "Splitting log file " + logfile.getPath() + "into a temporary staging area.");
303     Reader in = null;
304     this.fileBeingSplit = logfile;
305     try {
306       long logLength = logfile.getLen();
307       LOG.info("Splitting wal: " + logPath + ", length=" + logLength);
308       LOG.info("DistributedLogReplay = " + this.distributedLogReplay);
309       status.setStatus("Opening log file");
310       if (reporter != null && !reporter.progress()) {
311         progress_failed = true;
312         return false;
313       }
314       try {
315         in = getReader(logfile, skipErrors, reporter);
316       } catch (CorruptedLogFileException e) {
317         LOG.warn("Could not get reader, corrupted log file " + logPath, e);
318         ZKSplitLog.markCorrupted(walDir, logfile.getPath().getName(), walFS);
319         isCorrupted = true;
320       }
321       if (in == null) {
322         LOG.warn("Nothing to split in log file " + logPath);
323         return true;
324       }
325       if (csm != null) {
326         try {
327           TableStateManager tsm = csm.getTableStateManager();
328           disablingOrDisabledTables = tsm.getTablesInStates(
329           ZooKeeperProtos.Table.State.DISABLED, ZooKeeperProtos.Table.State.DISABLING);
330         } catch (CoordinatedStateException e) {
331           throw new IOException("Can't get disabling/disabled tables", e);
332         }
333       }
334       int numOpenedFilesBeforeReporting = conf.getInt("hbase.splitlog.report.openedfiles", 3);
335       int numOpenedFilesLastCheck = 0;
336       outputSink.setReporter(reporter);
337       outputSink.startWriterThreads();
338       outputSinkStarted = true;
339       Entry entry;
340       Long lastFlushedSequenceId = -1L;
341       ServerName serverName = DefaultWALProvider.getServerNameFromWALDirectoryName(logPath);
342       failedServerName = (serverName == null) ? "" : serverName.getServerName();
343       while ((entry = getNextLogLine(in, logPath, skipErrors)) != null) {
344         byte[] region = entry.getKey().getEncodedRegionName();
345         String encodedRegionNameAsStr = Bytes.toString(region);
346         lastFlushedSequenceId = lastFlushedSequenceIds.get(encodedRegionNameAsStr);
347         if (lastFlushedSequenceId == null) {
348           if (this.distributedLogReplay) {
349             RegionStoreSequenceIds ids =
350                 csm.getSplitLogWorkerCoordination().getRegionFlushedSequenceId(failedServerName,
351                   encodedRegionNameAsStr);
352             if (ids != null) {
353               lastFlushedSequenceId = ids.getLastFlushedSequenceId();
354               if (LOG.isDebugEnabled()) {
355                 LOG.debug("DLR Last flushed sequenceid for " + encodedRegionNameAsStr + ": " +
356                   TextFormat.shortDebugString(ids));
357               }
358             }
359           } else if (sequenceIdChecker != null) {
360             RegionStoreSequenceIds ids = sequenceIdChecker.getLastSequenceId(region);
361             Map<byte[], Long> maxSeqIdInStores = new TreeMap<byte[], Long>(Bytes.BYTES_COMPARATOR);
362             for (StoreSequenceId storeSeqId : ids.getStoreSequenceIdList()) {
363               maxSeqIdInStores.put(storeSeqId.getFamilyName().toByteArray(),
364                 storeSeqId.getSequenceId());
365             }
366             regionMaxSeqIdInStores.put(encodedRegionNameAsStr, maxSeqIdInStores);
367             lastFlushedSequenceId = ids.getLastFlushedSequenceId();
368             if (LOG.isDebugEnabled()) {
369               LOG.debug("DLS Last flushed sequenceid for " + encodedRegionNameAsStr + ": " +
370                   TextFormat.shortDebugString(ids));
371             }
372           }
373           if (lastFlushedSequenceId == null) {
374             lastFlushedSequenceId = -1L;
375           }
376           lastFlushedSequenceIds.put(encodedRegionNameAsStr, lastFlushedSequenceId);
377         }
378         if (lastFlushedSequenceId >= entry.getKey().getLogSeqNum()) {
379           editsSkipped++;
380           continue;
381         }
382         // Don't send Compaction/Close/Open region events to recovered edit type sinks.
383         if (entry.getEdit().isMetaEdit() && !outputSink.keepRegionEvent(entry)) {
384           editsSkipped++;
385           continue;
386         }
387         entryBuffers.appendEntry(entry);
388         editsCount++;
389         int moreWritersFromLastCheck = this.getNumOpenWriters() - numOpenedFilesLastCheck;
390         // If sufficient edits have passed, check if we should report progress.
391         if (editsCount % interval == 0
392             || moreWritersFromLastCheck > numOpenedFilesBeforeReporting) {
393           numOpenedFilesLastCheck = this.getNumOpenWriters();
394           String countsStr = (editsCount - (editsSkipped + outputSink.getSkippedEdits()))
395               + " edits, skipped " + editsSkipped + " edits.";
396           status.setStatus("Split " + countsStr);
397           if (reporter != null && !reporter.progress()) {
398             progress_failed = true;
399             return false;
400           }
401         }
402       }
403     } catch (InterruptedException ie) {
404       IOException iie = new InterruptedIOException();
405       iie.initCause(ie);
406       throw iie;
407     } catch (CorruptedLogFileException e) {
408       LOG.warn("Could not parse, corrupted log file " + logPath, e);
409       csm.getSplitLogWorkerCoordination().markCorrupted(walDir,
410         logfile.getPath().getName(), walFS);
411       isCorrupted = true;
412     } catch (IOException e) {
413       e = RemoteExceptionHandler.checkIOException(e);
414       throw e;
415     } finally {
416       LOG.debug("Finishing writing output logs and closing down.");
417       try {
418         if (null != in) {
419           in.close();
420         }
421       } catch (IOException exception) {
422         LOG.warn("Could not close wal reader: " + exception.getMessage());
423         LOG.debug("exception details", exception);
424       }
425       try {
426         if (outputSinkStarted) {
427           // Set progress_failed to true as the immediate following statement will reset its value
428           // when finishWritingAndClose() throws exception, progress_failed has the right value
429           progress_failed = true;
430           progress_failed = outputSink.finishWritingAndClose() == null;
431         }
432       } finally {
433         String msg =
434             "Processed " + editsCount + " edits across " + outputSink.getNumberOfRecoveredRegions()
435                 + " regions; edits skipped=" + editsSkipped + "; log file=" + logPath +
436                 ", length=" + logfile.getLen() + // See if length got updated post lease recovery
437                 ", corrupted=" + isCorrupted + ", progress failed=" + progress_failed;
438         LOG.info(msg);
439         status.markComplete(msg);
440       }
441     }
442     return !progress_failed;
443   }
444 
445   /**
446    * Completes the work done by splitLogFile by archiving logs
447    * <p>
448    * It is invoked by SplitLogManager once it knows that one of the
449    * SplitLogWorkers have completed the splitLogFile() part. If the master
450    * crashes then this function might get called multiple times.
451    * <p>
452    * @param logfile
453    * @param conf
454    * @throws IOException
455    */
456   public static void finishSplitLogFile(String logfile,
457       Configuration conf)  throws IOException {
458     Path walDir = FSUtils.getWALRootDir(conf);
459     Path oldLogDir = new Path(walDir, HConstants.HREGION_OLDLOGDIR_NAME);
460     Path logPath;
461     if (FSUtils.isStartingWithPath(walDir, logfile)) {
462       logPath = new Path(logfile);
463     } else {
464       logPath = new Path(walDir, logfile);
465     }
466     finishSplitLogFile(walDir, oldLogDir, logPath, conf);
467   }
468 
469   private static void finishSplitLogFile(Path walDir, Path oldLogDir,
470       Path logPath, Configuration conf) throws IOException {
471     List<Path> processedLogs = new ArrayList<Path>();
472     List<Path> corruptedLogs = new ArrayList<Path>();
473     FileSystem walFS = walDir.getFileSystem(conf);
474     if (ZKSplitLog.isCorrupted(walDir, logPath.getName(), walFS)) {
475       corruptedLogs.add(logPath);
476     } else {
477       processedLogs.add(logPath);
478     }
479     archiveLogs(corruptedLogs, processedLogs, oldLogDir, walFS, conf);
480     Path stagingDir = ZKSplitLog.getSplitLogDir(walDir, logPath.getName());
481     walFS.delete(stagingDir, true);
482   }
483 
484   /**
485    * Moves processed logs to a oldLogDir after successful processing Moves
486    * corrupted logs (any log that couldn't be successfully parsed to corruptDir
487    * (.corrupt) for later investigation
488    *
489    * @param corruptedLogs
490    * @param processedLogs
491    * @param oldLogDir
492    * @param walFS FileSystem to use for WAL archival
493    * @param conf
494    * @throws IOException
495    */
496   private static void archiveLogs(
497       final List<Path> corruptedLogs,
498       final List<Path> processedLogs, final Path oldLogDir,
499       final FileSystem walFS, final Configuration conf) throws IOException {
500     final Path corruptDir = new Path(FSUtils.getWALRootDir(conf), conf.get(
501         "hbase.regionserver.hlog.splitlog.corrupt.dir",  HConstants.CORRUPT_DIR_NAME));
502 
503     if (!walFS.mkdirs(corruptDir)) {
504       LOG.info("Unable to mkdir " + corruptDir);
505     }
506     walFS.mkdirs(oldLogDir);
507 
508     // this method can get restarted or called multiple times for archiving
509     // the same log files.
510     for (Path corrupted : corruptedLogs) {
511       Path p = new Path(corruptDir, corrupted.getName());
512       if (walFS.exists(corrupted)) {
513         if (!walFS.rename(corrupted, p)) {
514           LOG.warn("Unable to move corrupted log " + corrupted + " to " + p);
515         } else {
516           LOG.warn("Moved corrupted log " + corrupted + " to " + p);
517         }
518       }
519     }
520 
521     for (Path p : processedLogs) {
522       Path newPath = FSHLog.getWALArchivePath(oldLogDir, p);
523       if (walFS.exists(p)) {
524         if (!FSUtils.renameAndSetModifyTime(walFS, p, newPath)) {
525           LOG.warn("Unable to move  " + p + " to " + newPath);
526         } else {
527           LOG.info("Archived processed log " + p + " to " + newPath);
528         }
529       }
530     }
531   }
532 
533   /**
534    * Path to a file under RECOVERED_EDITS_DIR directory of the region found in
535    * <code>logEntry</code> named for the sequenceid in the passed
536    * <code>logEntry</code>: e.g. /hbase/some_table/2323432434/recovered.edits/2332.
537    * This method also ensures existence of RECOVERED_EDITS_DIR under the region
538    * creating it if necessary.
539    * @param logEntry
540    * @param fileNameBeingSplit the file being split currently. Used to generate tmp file name.
541    * @param tmpDirName of the directory used to sideline old recovered edits file
542    * @param conf
543    * @return Path to file into which to dump split log edits.
544    * @throws IOException
545    */
546   @SuppressWarnings("deprecation")
547   @VisibleForTesting
548   static Path getRegionSplitEditsPath(final Entry logEntry, String fileNameBeingSplit,
549       String tmpDirName, Configuration conf) throws IOException {
550     FileSystem walFS = FSUtils.getWALFileSystem(conf);
551     Path tableDir = FSUtils.getWALTableDir(conf, logEntry.getKey().getTablename());
552     String encodedRegionName = Bytes.toString(logEntry.getKey().getEncodedRegionName());
553     Path regionDir = HRegion.getRegionDir(tableDir, encodedRegionName);
554     Path dir = getRegionDirRecoveredEditsDir(regionDir);
555 
556     if (walFS.exists(dir) && walFS.isFile(dir)) {
557       Path tmp = new Path(tmpDirName);
558       if (!walFS.exists(tmp)) {
559         walFS.mkdirs(tmp);
560       }
561       tmp = new Path(tmp,
562         HConstants.RECOVERED_EDITS_DIR + "_" + encodedRegionName);
563       LOG.warn("Found existing old file: " + dir + ". It could be some "
564         + "leftover of an old installation. It should be a folder instead. "
565         + "So moving it to " + tmp);
566       if (!walFS.rename(dir, tmp)) {
567         LOG.warn("Failed to sideline old file " + dir);
568       }
569     }
570 
571     if (!walFS.exists(dir) && !walFS.mkdirs(dir)) {
572       LOG.warn("mkdir failed on " + dir);
573     }
574     // Append fileBeingSplit to prevent name conflict since we may have duplicate wal entries now.
575     // Append file name ends with RECOVERED_LOG_TMPFILE_SUFFIX to ensure
576     // region's replayRecoveredEdits will not delete it
577     String fileName = formatRecoveredEditsFileName(logEntry.getKey().getLogSeqNum());
578     fileName = getTmpRecoveredEditsFileName(fileName + "-" + fileNameBeingSplit);
579     return new Path(dir, fileName);
580   }
581 
582   private static String getTmpRecoveredEditsFileName(String fileName) {
583     return fileName + RECOVERED_LOG_TMPFILE_SUFFIX;
584   }
585 
586   /**
587    * Get the completed recovered edits file path, renaming it to be by last edit
588    * in the file from its first edit. Then we could use the name to skip
589    * recovered edits when doing {@link HRegion#replayRecoveredEditsIfAny}.
590    * @param srcPath
591    * @param maximumEditLogSeqNum
592    * @return dstPath take file's last edit log seq num as the name
593    */
594   private static Path getCompletedRecoveredEditsFilePath(Path srcPath,
595       long maximumEditLogSeqNum) {
596     String fileName = formatRecoveredEditsFileName(maximumEditLogSeqNum);
597     return new Path(srcPath.getParent(), fileName);
598   }
599 
600   @VisibleForTesting
601   static String formatRecoveredEditsFileName(final long seqid) {
602     return String.format("%019d", seqid);
603   }
604 
605   private static final Pattern EDITFILES_NAME_PATTERN = Pattern.compile("-?[0-9]+");
606   private static final String RECOVERED_LOG_TMPFILE_SUFFIX = ".temp";
607 
608   /**
609    * @param regionDir
610    *          This regions directory in the filesystem.
611    * @return The directory that holds recovered edits files for the region
612    *         <code>regionDir</code>
613    */
614   public static Path getRegionDirRecoveredEditsDir(final Path regionDir) {
615     return new Path(regionDir, HConstants.RECOVERED_EDITS_DIR);
616   }
617 
618   /**
619    * Returns sorted set of edit files made by splitter, excluding files
620    * with '.temp' suffix.
621    *
622    * @param walFS FileSystem to use for reading Recovered edits files
623    * @param regionDir Directory where Recovered edits should reside
624    * @return Files in passed <code>regionDir</code> as a sorted set.
625    * @throws IOException
626    */
627   public static NavigableSet<Path> getSplitEditFilesSorted(final FileSystem walFS,
628       final Path regionDir) throws IOException {
629     NavigableSet<Path> filesSorted = new TreeSet<Path>();
630     Path editsdir = getRegionDirRecoveredEditsDir(regionDir);
631     if (!walFS.exists(editsdir)) {
632       return filesSorted;
633     }
634     FileStatus[] files = FSUtils.listStatus(walFS, editsdir, new PathFilter() {
635       @Override
636       public boolean accept(Path p) {
637         boolean result = false;
638         try {
639           // Return files and only files that match the editfile names pattern.
640           // There can be other files in this directory other than edit files.
641           // In particular, on error, we'll move aside the bad edit file giving
642           // it a timestamp suffix. See moveAsideBadEditsFile.
643           Matcher m = EDITFILES_NAME_PATTERN.matcher(p.getName());
644           result = walFS.isFile(p) && m.matches();
645           // Skip the file whose name ends with RECOVERED_LOG_TMPFILE_SUFFIX,
646           // because it means splitwal thread is writting this file.
647           if (p.getName().endsWith(RECOVERED_LOG_TMPFILE_SUFFIX)) {
648             result = false;
649           }
650           // Skip SeqId Files
651           if (isSequenceIdFile(p)) {
652             result = false;
653           }
654         } catch (IOException e) {
655           LOG.warn("Failed isFile check on " + p);
656         }
657         return result;
658       }
659     });
660     if (files == null) {
661       return filesSorted;
662     }
663     for (FileStatus status : files) {
664       filesSorted.add(status.getPath());
665     }
666     return filesSorted;
667   }
668 
669   /**
670    * Move aside a bad edits file.
671    *
672    * @param walFS FileSystem to use for WAL operations
673    * @param edits
674    *          Edits file to move aside.
675    * @return The name of the moved aside file.
676    * @throws IOException
677    */
678   public static Path moveAsideBadEditsFile(final FileSystem walFS, final Path edits)
679       throws IOException {
680     Path moveAsideName = new Path(edits.getParent(), edits.getName() + "."
681         + System.currentTimeMillis());
682     if (!walFS.rename(edits, moveAsideName)) {
683       LOG.warn("Rename failed from " + edits + " to " + moveAsideName);
684     }
685     return moveAsideName;
686   }
687 
688   private static final String SEQUENCE_ID_FILE_SUFFIX = ".seqid";
689   private static final String OLD_SEQUENCE_ID_FILE_SUFFIX = "_seqid";
690   private static final int SEQUENCE_ID_FILE_SUFFIX_LENGTH = SEQUENCE_ID_FILE_SUFFIX.length();
691 
692   /**
693    * Is the given file a region open sequence id file.
694    */
695   @VisibleForTesting
696   public static boolean isSequenceIdFile(final Path file) {
697     return file.getName().endsWith(SEQUENCE_ID_FILE_SUFFIX)
698         || file.getName().endsWith(OLD_SEQUENCE_ID_FILE_SUFFIX);
699   }
700 
701   /**
702    * Create a file with name as region open sequence id
703    * @param walFS FileSystem to write Sequence file to
704    * @param regionDir WALRegionDir used to determine where to write edits files
705    * @param newSeqId
706    * @param saftyBumper
707    * @return long new sequence Id value
708    * @throws IOException
709    */
710   public static long writeRegionSequenceIdFile(final FileSystem walFS, final Path regionDir,
711       long newSeqId, long saftyBumper) throws IOException {
712 
713     Path editsdir = WALSplitter.getRegionDirRecoveredEditsDir(regionDir);
714     long maxSeqId = 0;
715     FileStatus[] files = null;
716     if (walFS.exists(editsdir)) {
717       files = FSUtils.listStatus(walFS, editsdir, new PathFilter() {
718         @Override
719         public boolean accept(Path p) {
720           return isSequenceIdFile(p);
721         }
722       });
723       if (files != null) {
724         for (FileStatus status : files) {
725           String fileName = status.getPath().getName();
726           try {
727             Long tmpSeqId = Long.parseLong(fileName.substring(0, fileName.length()
728                 - SEQUENCE_ID_FILE_SUFFIX_LENGTH));
729             maxSeqId = Math.max(tmpSeqId, maxSeqId);
730           } catch (NumberFormatException ex) {
731             LOG.warn("Invalid SeqId File Name=" + fileName);
732           }
733         }
734       }
735     }
736     if (maxSeqId > newSeqId) {
737       newSeqId = maxSeqId;
738     }
739     newSeqId += saftyBumper; // bump up SeqId
740 
741     // write a new seqId file
742     Path newSeqIdFile = new Path(editsdir, newSeqId + SEQUENCE_ID_FILE_SUFFIX);
743     if (newSeqId != maxSeqId) {
744       try {
745         if (!walFS.createNewFile(newSeqIdFile) && !walFS.exists(newSeqIdFile)) {
746           throw new IOException("Failed to create SeqId file:" + newSeqIdFile);
747         }
748         if (LOG.isDebugEnabled()) {
749           LOG.debug("Wrote region seqId=" + newSeqIdFile + " to file, newSeqId=" + newSeqId
750               + ", maxSeqId=" + maxSeqId);
751         }
752       } catch (FileAlreadyExistsException ignored) {
753         // latest hdfs throws this exception. it's all right if newSeqIdFile already exists
754       }
755     }
756     // remove old ones
757     if (files != null) {
758       for (FileStatus status : files) {
759         if (newSeqIdFile.equals(status.getPath())) {
760           continue;
761         }
762         walFS.delete(status.getPath(), false);
763       }
764     }
765     return newSeqId;
766   }
767 
768   /**
769    * Create a new {@link Reader} for reading logs to split.
770    *
771    * @param file
772    * @return A new Reader instance, caller should close
773    * @throws IOException
774    * @throws CorruptedLogFileException
775    */
776   protected Reader getReader(FileStatus file, boolean skipErrors, CancelableProgressable reporter)
777       throws IOException, CorruptedLogFileException {
778     Path path = file.getPath();
779     long length = file.getLen();
780     Reader in;
781 
782     // Check for possibly empty file. With appends, currently Hadoop reports a
783     // zero length even if the file has been sync'd. Revisit if HDFS-376 or
784     // HDFS-878 is committed.
785     if (length <= 0) {
786       LOG.warn("File " + path + " might be still open, length is 0");
787     }
788 
789     try {
790       FSUtils.getInstance(walFS, conf).recoverFileLease(walFS, path, conf, reporter);
791       try {
792         in = getReader(path, reporter);
793       } catch (EOFException e) {
794         if (length <= 0) {
795           // TODO should we ignore an empty, not-last log file if skip.errors
796           // is false? Either way, the caller should decide what to do. E.g.
797           // ignore if this is the last log in sequence.
798           // TODO is this scenario still possible if the log has been
799           // recovered (i.e. closed)
800           LOG.warn("Could not open " + path + " for reading. File is empty", e);
801           return null;
802         } else {
803           // EOFException being ignored
804           return null;
805         }
806       }
807     } catch (IOException e) {
808       if (e instanceof FileNotFoundException) {
809         // A wal file may not exist anymore. Nothing can be recovered so move on
810         LOG.warn("File " + path + " doesn't exist anymore.", e);
811         return null;
812       }
813       if (!skipErrors || e instanceof InterruptedIOException) {
814         throw e; // Don't mark the file corrupted if interrupted, or not skipErrors
815       }
816       CorruptedLogFileException t =
817         new CorruptedLogFileException("skipErrors=true Could not open wal " +
818             path + " ignoring");
819       t.initCause(e);
820       throw t;
821     }
822     return in;
823   }
824 
825   static private Entry getNextLogLine(Reader in, Path path, boolean skipErrors)
826   throws CorruptedLogFileException, IOException {
827     try {
828       return in.next();
829     } catch (EOFException eof) {
830       // truncated files are expected if a RS crashes (see HBASE-2643)
831       LOG.info("EOF from wal " + path + ".  continuing");
832       return null;
833     } catch (IOException e) {
834       // If the IOE resulted from bad file format,
835       // then this problem is idempotent and retrying won't help
836       if (e.getCause() != null &&
837           (e.getCause() instanceof ParseException ||
838            e.getCause() instanceof org.apache.hadoop.fs.ChecksumException)) {
839         LOG.warn("Parse exception " + e.getCause().toString() + " from wal "
840            + path + ".  continuing");
841         return null;
842       }
843       if (!skipErrors) {
844         throw e;
845       }
846       CorruptedLogFileException t =
847         new CorruptedLogFileException("skipErrors=true Ignoring exception" +
848             " while parsing wal " + path + ". Marking as corrupted");
849       t.initCause(e);
850       throw t;
851     }
852   }
853 
854   /**
855    * Create a new {@link Writer} for writing log splits.
856    * @return a new Writer instance, caller should close
857    */
858   protected Writer createWriter(Path logfile)
859       throws IOException {
860     return walFactory.createRecoveredEditsWriter(walFS, logfile);
861   }
862 
863   /**
864    * Create a new {@link Reader} for reading logs to split.
865    * @return new Reader instance, caller should close
866    */
867   protected Reader getReader(Path curLogFile, CancelableProgressable reporter) throws IOException {
868     return walFactory.createReader(walFS, curLogFile, reporter);
869   }
870 
871   /**
872    * Get current open writers
873    */
874   private int getNumOpenWriters() {
875     int result = 0;
876     if (this.outputSink != null) {
877       result += this.outputSink.getNumOpenWriters();
878     }
879     return result;
880   }
881 
882   /**
883    * Contains some methods to control WAL-entries producer / consumer interactions
884    */
885   public static class PipelineController {
886     // If an exception is thrown by one of the other threads, it will be
887     // stored here.
888     AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();
889 
890     // Wait/notify for when data has been produced by the writer thread,
891     // consumed by the reader thread, or an exception occurred
892     public final Object dataAvailable = new Object();
893 
894     void writerThreadError(Throwable t) {
895       thrown.compareAndSet(null, t);
896     }
897 
898     /**
899      * Check for errors in the writer threads. If any is found, rethrow it.
900      */
901     void checkForErrors() throws IOException {
902       Throwable thrown = this.thrown.get();
903       if (thrown == null) return;
904       if (thrown instanceof IOException) {
905         throw new IOException(thrown);
906       } else {
907         throw new RuntimeException(thrown);
908       }
909     }
910   }
911 
912   /**
913    * Class which accumulates edits and separates them into a buffer per region
914    * while simultaneously accounting RAM usage. Blocks if the RAM usage crosses
915    * a predefined threshold.
916    *
917    * Writer threads then pull region-specific buffers from this class.
918    */
919   public static class EntryBuffers {
920     PipelineController controller;
921 
922     Map<byte[], RegionEntryBuffer> buffers =
923       new TreeMap<byte[], RegionEntryBuffer>(Bytes.BYTES_COMPARATOR);
924 
925     /* Track which regions are currently in the middle of writing. We don't allow
926        an IO thread to pick up bytes from a region if we're already writing
927        data for that region in a different IO thread. */
928     Set<byte[]> currentlyWriting = new TreeSet<byte[]>(Bytes.BYTES_COMPARATOR);
929 
930     long totalBuffered = 0;
931     final long maxHeapUsage;
932     boolean splitWriterCreationBounded;
933 
934 
935     public EntryBuffers(PipelineController controller, long maxHeapUsage) {
936       this(controller, maxHeapUsage, false);
937     }
938 
939     public EntryBuffers(PipelineController controller, long maxHeapUsage,
940         boolean splitWriterCreationBounded) {
941       this.controller = controller;
942       this.maxHeapUsage = maxHeapUsage;
943       this.splitWriterCreationBounded = splitWriterCreationBounded;
944     }
945 
946     /**
947      * Append a log entry into the corresponding region buffer.
948      * Blocks if the total heap usage has crossed the specified threshold.
949      *
950      * @throws InterruptedException
951      * @throws IOException
952      */
953     public void appendEntry(Entry entry) throws InterruptedException, IOException {
954       WALKey key = entry.getKey();
955 
956       RegionEntryBuffer buffer;
957       long incrHeap;
958       synchronized (this) {
959         buffer = buffers.get(key.getEncodedRegionName());
960         if (buffer == null) {
961           buffer = new RegionEntryBuffer(key.getTablename(), key.getEncodedRegionName());
962           buffers.put(key.getEncodedRegionName(), buffer);
963         }
964         incrHeap= buffer.appendEntry(entry);
965       }
966 
967       // If we crossed the chunk threshold, wait for more space to be available
968       synchronized (controller.dataAvailable) {
969         totalBuffered += incrHeap;
970         while (totalBuffered > maxHeapUsage && controller.thrown.get() == null) {
971           LOG.debug("Used " + totalBuffered + " bytes of buffered edits, waiting for IO threads...");
972           controller.dataAvailable.wait(2000);
973         }
974         controller.dataAvailable.notifyAll();
975       }
976       controller.checkForErrors();
977     }
978 
979     /**
980      * @return RegionEntryBuffer a buffer of edits to be written or replayed.
981      */
982     synchronized RegionEntryBuffer getChunkToWrite() {
983       // The core part of limiting opening writers is it doesn't return chunk only if the heap size
984       // is over maxHeapUsage. Thus it doesn't need to create a writer for each region
985       // during splitting. It will flush all the logs in the buffer after splitting through a
986       // threadpool, which means the number of writers it created is under control
987       if(splitWriterCreationBounded && totalBuffered < maxHeapUsage){
988         return null;
989       }
990 
991       long biggestSize = 0;
992       byte[] biggestBufferKey = null;
993 
994       for (Map.Entry<byte[], RegionEntryBuffer> entry : buffers.entrySet()) {
995         long size = entry.getValue().heapSize();
996         if (size > biggestSize && (!currentlyWriting.contains(entry.getKey()))) {
997           biggestSize = size;
998           biggestBufferKey = entry.getKey();
999         }
1000       }
1001       if (biggestBufferKey == null) {
1002         return null;
1003       }
1004 
1005       RegionEntryBuffer buffer = buffers.remove(biggestBufferKey);
1006       currentlyWriting.add(biggestBufferKey);
1007       return buffer;
1008     }
1009 
1010     void doneWriting(RegionEntryBuffer buffer) {
1011       synchronized (this) {
1012         boolean removed = currentlyWriting.remove(buffer.encodedRegionName);
1013         assert removed;
1014       }
1015       long size = buffer.heapSize();
1016 
1017       synchronized (controller.dataAvailable) {
1018         totalBuffered -= size;
1019         // We may unblock writers
1020         controller.dataAvailable.notifyAll();
1021       }
1022     }
1023 
1024     synchronized boolean isRegionCurrentlyWriting(byte[] region) {
1025       return currentlyWriting.contains(region);
1026     }
1027 
1028     public void waitUntilDrained() {
1029       synchronized (controller.dataAvailable) {
1030         while (totalBuffered > 0) {
1031           try {
1032             controller.dataAvailable.wait(2000);
1033           } catch (InterruptedException e) {
1034             LOG.warn("Got interrupted while waiting for EntryBuffers is drained");
1035             Thread.interrupted();
1036             break;
1037           }
1038         }
1039       }
1040     }
1041   }
1042 
1043   /**
1044    * A buffer of some number of edits for a given region.
1045    * This accumulates edits and also provides a memory optimization in order to
1046    * share a single byte array instance for the table and region name.
1047    * Also tracks memory usage of the accumulated edits.
1048    */
1049   public static class RegionEntryBuffer implements HeapSize {
1050     long heapInBuffer = 0;
1051     List<Entry> entryBuffer;
1052     TableName tableName;
1053     byte[] encodedRegionName;
1054 
1055     RegionEntryBuffer(TableName tableName, byte[] region) {
1056       this.tableName = tableName;
1057       this.encodedRegionName = region;
1058       this.entryBuffer = new LinkedList<Entry>();
1059     }
1060 
1061     long appendEntry(Entry entry) {
1062       internify(entry);
1063       entryBuffer.add(entry);
1064       long incrHeap = entry.getEdit().heapSize() +
1065         ClassSize.align(2 * ClassSize.REFERENCE) + // WALKey pointers
1066         0; // TODO linkedlist entry
1067       heapInBuffer += incrHeap;
1068       return incrHeap;
1069     }
1070 
1071     private void internify(Entry entry) {
1072       WALKey k = entry.getKey();
1073       k.internTableName(this.tableName);
1074       k.internEncodedRegionName(this.encodedRegionName);
1075     }
1076 
1077     @Override
1078     public long heapSize() {
1079       return heapInBuffer;
1080     }
1081 
1082     public byte[] getEncodedRegionName() {
1083       return encodedRegionName;
1084     }
1085 
1086     public List<Entry> getEntryBuffer() {
1087       return entryBuffer;
1088     }
1089 
1090     public TableName getTableName() {
1091       return tableName;
1092     }
1093   }
1094 
1095   public static class WriterThread extends Thread {
1096     private volatile boolean shouldStop = false;
1097     private PipelineController controller;
1098     private EntryBuffers entryBuffers;
1099     private OutputSink outputSink = null;
1100 
1101     WriterThread(PipelineController controller, EntryBuffers entryBuffers, OutputSink sink, int i){
1102       super(Thread.currentThread().getName() + "-Writer-" + i);
1103       this.controller = controller;
1104       this.entryBuffers = entryBuffers;
1105       outputSink = sink;
1106     }
1107 
1108     @Override
1109     public void run()  {
1110       try {
1111         doRun();
1112       } catch (Throwable t) {
1113         LOG.error("Exiting thread", t);
1114         controller.writerThreadError(t);
1115       }
1116     }
1117 
1118     private void doRun() throws IOException {
1119       if (LOG.isTraceEnabled()) LOG.trace("Writer thread starting");
1120       while (true) {
1121         RegionEntryBuffer buffer = entryBuffers.getChunkToWrite();
1122         if (buffer == null) {
1123           // No data currently available, wait on some more to show up
1124           synchronized (controller.dataAvailable) {
1125             if (shouldStop && !this.outputSink.flush()) {
1126               return;
1127             }
1128             try {
1129               controller.dataAvailable.wait(500);
1130             } catch (InterruptedException ie) {
1131               if (!shouldStop) {
1132                 throw new RuntimeException(ie);
1133               }
1134             }
1135           }
1136           continue;
1137         }
1138 
1139         assert buffer != null;
1140         try {
1141           writeBuffer(buffer);
1142         } finally {
1143           entryBuffers.doneWriting(buffer);
1144         }
1145       }
1146     }
1147 
1148     private void writeBuffer(RegionEntryBuffer buffer) throws IOException {
1149       outputSink.append(buffer);
1150     }
1151 
1152     void finish() {
1153       synchronized (controller.dataAvailable) {
1154         shouldStop = true;
1155         controller.dataAvailable.notifyAll();
1156       }
1157     }
1158   }
1159 
1160   /**
1161    * The following class is an abstraction class to provide a common interface to support both
1162    * existing recovered edits file sink and region server WAL edits replay sink
1163    */
1164   public static abstract class OutputSink {
1165 
1166     protected PipelineController controller;
1167     protected EntryBuffers entryBuffers;
1168 
1169     protected ConcurrentHashMap<String, SinkWriter> writers = new ConcurrentHashMap<>();
1170     protected ConcurrentHashMap<String, Long> regionMaximumEditLogSeqNum =
1171         new ConcurrentHashMap<>();
1172 
1173     protected final List<WriterThread> writerThreads = Lists.newArrayList();
1174 
1175     /* Set of regions which we've decided should not output edits */
1176     protected final Set<byte[]> blacklistedRegions = Collections
1177         .synchronizedSet(new TreeSet<byte[]>(Bytes.BYTES_COMPARATOR));
1178 
1179     protected boolean closeAndCleanCompleted = false;
1180 
1181     protected boolean writersClosed = false;
1182 
1183     protected final int numThreads;
1184 
1185     protected CancelableProgressable reporter = null;
1186 
1187     protected AtomicLong skippedEdits = new AtomicLong();
1188 
1189     protected List<Path> splits = null;
1190 
1191     public OutputSink(PipelineController controller, EntryBuffers entryBuffers, int numWriters) {
1192       numThreads = numWriters;
1193       this.controller = controller;
1194       this.entryBuffers = entryBuffers;
1195     }
1196 
1197     void setReporter(CancelableProgressable reporter) {
1198       this.reporter = reporter;
1199     }
1200 
1201     /**
1202      * Start the threads that will pump data from the entryBuffers to the output files.
1203      */
1204     public synchronized void startWriterThreads() {
1205       for (int i = 0; i < numThreads; i++) {
1206         WriterThread t = new WriterThread(controller, entryBuffers, this, i);
1207         t.start();
1208         writerThreads.add(t);
1209       }
1210     }
1211 
1212     /**
1213      *
1214      * Update region's maximum edit log SeqNum.
1215      */
1216     void updateRegionMaximumEditLogSeqNum(Entry entry) {
1217 
1218       synchronized (regionMaximumEditLogSeqNum) {
1219         String encodedRegionName = Bytes.toString(entry.getKey().getEncodedRegionName());
1220         Long currentMaxSeqNum = regionMaximumEditLogSeqNum.get(encodedRegionName);
1221         if (currentMaxSeqNum == null || entry.getKey().getLogSeqNum() > currentMaxSeqNum) {
1222           regionMaximumEditLogSeqNum.put(encodedRegionName, entry.getKey()
1223               .getLogSeqNum());
1224         }
1225       }
1226     }
1227 
1228     Long getRegionMaximumEditLogSeqNum(byte[] region) {
1229       return regionMaximumEditLogSeqNum.get(Bytes.toString(region));
1230     }
1231 
1232     /**
1233      * @return the number of currently opened writers
1234      */
1235     int getNumOpenWriters() {
1236       return this.writers.size();
1237     }
1238 
1239     long getSkippedEdits() {
1240       return this.skippedEdits.get();
1241     }
1242 
1243     /**
1244      * Wait for writer threads to dump all info to the sink
1245      * @return true when there is no error
1246      * @throws IOException
1247      */
1248     protected boolean finishWriting(boolean interrupt) throws IOException {
1249       LOG.debug("Waiting for split writer threads to finish");
1250       boolean progress_failed = false;
1251       for (WriterThread t : writerThreads) {
1252         t.finish();
1253       }
1254       if (interrupt) {
1255         for (WriterThread t : writerThreads) {
1256           t.interrupt(); // interrupt the writer threads. We are stopping now.
1257         }
1258       }
1259 
1260       for (WriterThread t : writerThreads) {
1261         if (!progress_failed && reporter != null && !reporter.progress()) {
1262           progress_failed = true;
1263         }
1264         try {
1265           t.join();
1266         } catch (InterruptedException ie) {
1267           IOException iie = new InterruptedIOException();
1268           iie.initCause(ie);
1269           throw iie;
1270         }
1271       }
1272       controller.checkForErrors();
1273       LOG.info(this.writerThreads.size() + " split writers finished; closing...");
1274       return (!progress_failed);
1275     }
1276 
1277     public abstract List<Path> finishWritingAndClose() throws IOException;
1278 
1279     /**
1280      * @return a map from encoded region ID to the number of edits written out for that region.
1281      */
1282     public abstract Map<byte[], Long> getOutputCounts();
1283 
1284     /**
1285      * @return number of regions we've recovered
1286      */
1287     public abstract int getNumberOfRecoveredRegions();
1288 
1289     /**
1290      * @param buffer A WAL Edit Entry
1291      * @throws IOException
1292      */
1293     public abstract void append(RegionEntryBuffer buffer) throws IOException;
1294 
1295     /**
1296      * WriterThread call this function to help flush internal remaining edits in buffer before close
1297      * @return true when underlying sink has something to flush
1298      */
1299     public boolean flush() throws IOException {
1300       return false;
1301     }
1302 
1303     /**
1304      * Some WALEdit's contain only KV's for account on what happened to a region.
1305      * Not all sinks will want to get all of those edits.
1306      *
1307      * @return Return true if this sink wants to accept this region-level WALEdit.
1308      */
1309     public abstract boolean keepRegionEvent(Entry entry);
1310   }
1311 
1312   /**
1313    * Class that manages the output streams from the log splitting process.
1314    */
1315   class LogRecoveredEditsOutputSink extends OutputSink {
1316 
1317     public LogRecoveredEditsOutputSink(PipelineController controller, EntryBuffers entryBuffers,
1318         int numWriters) {
1319       // More threads could potentially write faster at the expense
1320       // of causing more disk seeks as the logs are split.
1321       // 3. After a certain setting (probably around 3) the
1322       // process will be bound on the reader in the current
1323       // implementation anyway.
1324       super(controller, entryBuffers, numWriters);
1325     }
1326 
1327     /**
1328      * @return null if failed to report progress
1329      * @throws IOException
1330      */
1331     @Override
1332     public List<Path> finishWritingAndClose() throws IOException {
1333       boolean isSuccessful = false;
1334       List<Path> result = null;
1335       try {
1336         isSuccessful = finishWriting(false);
1337       } finally {
1338         result = close();
1339         List<IOException> thrown = closeLogWriters(null);
1340         if (thrown != null && !thrown.isEmpty()) {
1341           throw MultipleIOException.createIOException(thrown);
1342         }
1343       }
1344       if (isSuccessful) {
1345         splits = result;
1346       }
1347       return splits;
1348     }
1349 
1350     // delete the one with fewer wal entries
1351     void deleteOneWithFewerEntries(WriterAndPath wap, Path dst)
1352         throws IOException {
1353       long dstMinLogSeqNum = -1L;
1354       try (WAL.Reader reader = walFactory.createReader(walFS, dst)) {
1355         WAL.Entry entry = reader.next();
1356         if (entry != null) {
1357           dstMinLogSeqNum = entry.getKey().getLogSeqNum();
1358         }
1359       } catch (EOFException e) {
1360         if (LOG.isDebugEnabled()) {
1361           LOG.debug(
1362             "Got EOF when reading first WAL entry from " + dst + ", an empty or broken WAL file?",
1363             e);
1364         }
1365       }
1366       if (wap.minLogSeqNum < dstMinLogSeqNum) {
1367         LOG.warn("Found existing old edits file. It could be the result of a previous failed"
1368             + " split attempt or we have duplicated wal entries. Deleting " + dst + ", length="
1369             + walFS.getFileStatus(dst).getLen());
1370         if (!walFS.delete(dst, false)) {
1371           LOG.warn("Failed deleting of old " + dst);
1372           throw new IOException("Failed deleting of old " + dst);
1373         }
1374       } else {
1375         LOG.warn("Found existing old edits file and we have less entries. Deleting " + wap.p
1376             + ", length=" + walFS.getFileStatus(wap.p).getLen());
1377         if (!walFS.delete(wap.p, false)) {
1378           LOG.warn("Failed deleting of " + wap.p);
1379           throw new IOException("Failed deleting of " + wap.p);
1380         }
1381       }
1382     }
1383 
1384     /**
1385      * Close all of the output streams.
1386      * @return the list of paths written.
1387      */
1388     List<Path> close() throws IOException {
1389       Preconditions.checkState(!closeAndCleanCompleted);
1390 
1391       final List<Path> paths = new ArrayList<Path>();
1392       final List<IOException> thrown = Lists.newArrayList();
1393       ThreadPoolExecutor closeThreadPool = Threads.getBoundedCachedThreadPool(numThreads, 30L,
1394         TimeUnit.SECONDS, new ThreadFactory() {
1395           private int count = 1;
1396 
1397           @Override
1398           public Thread newThread(Runnable r) {
1399             Thread t = new Thread(r, "split-log-closeStream-" + count++);
1400             return t;
1401           }
1402         });
1403       CompletionService<Void> completionService =
1404         new ExecutorCompletionService<Void>(closeThreadPool);
1405       boolean progress_failed;
1406       try{
1407         progress_failed = executeCloseTask(completionService, thrown, paths);
1408       } catch (InterruptedException e) {
1409         IOException iie = new InterruptedIOException();
1410         iie.initCause(e);
1411         throw iie;
1412       } catch (ExecutionException e) {
1413         throw new IOException(e.getCause());
1414       } finally {
1415         closeThreadPool.shutdownNow();
1416       }
1417 
1418       if (!thrown.isEmpty()) {
1419         throw MultipleIOException.createIOException(thrown);
1420       }
1421       writersClosed = true;
1422       closeAndCleanCompleted = true;
1423       if (progress_failed) {
1424         return null;
1425       }
1426       return paths;
1427     }
1428 
1429     boolean executeCloseTask(CompletionService<Void> completionService,
1430         final List<IOException> thrown, final List<Path> paths)
1431         throws InterruptedException, ExecutionException {
1432       for (final Map.Entry<String, SinkWriter> writersEntry : writers.entrySet()) {
1433         if (LOG.isTraceEnabled()) {
1434           LOG.trace("Submitting close of " + ((WriterAndPath) writersEntry.getValue()).p);
1435         }
1436         completionService.submit(new Callable<Void>() {
1437           @Override public Void call() throws Exception {
1438             WriterAndPath wap = (WriterAndPath) writersEntry.getValue();
1439             Path dst = closeWriter(writersEntry.getKey(), wap, thrown);
1440             paths.add(dst);
1441             return null;
1442           }
1443         });
1444       }
1445       boolean progress_failed = false;
1446       for (int i = 0, n = this.writers.size(); i < n; i++) {
1447         Future<Void> future = completionService.take();
1448         future.get();
1449         if (!progress_failed && reporter != null && !reporter.progress()) {
1450           progress_failed = true;
1451         }
1452       }
1453       return progress_failed;
1454     }
1455 
1456     Path closeWriter(String encodedRegionName, WriterAndPath wap, List<IOException> thrown)
1457         throws IOException {
1458       if (LOG.isTraceEnabled()) {
1459         LOG.trace("Closing " + wap.p);
1460       }
1461 
1462       try {
1463         wap.w.close();
1464       } catch (IOException ioe) {
1465         LOG.error("Couldn't close log at " + wap.p, ioe);
1466         thrown.add(ioe);
1467         return null;
1468       }
1469       if (LOG.isDebugEnabled()) {
1470         LOG.debug("Closed wap " + wap.p + " (wrote " + wap.editsWritten
1471             + " edits, skipped " + wap.editsSkipped + " edits in "
1472             + (wap.nanosSpent / 1000 / 1000) + "ms");
1473       }
1474       if (wap.editsWritten == 0) {
1475         // just remove the empty recovered.edits file
1476         if (walFS.exists(wap.p) && !walFS.delete(wap.p, false)) {
1477           LOG.warn("Failed deleting empty " + wap.p);
1478           throw new IOException("Failed deleting empty  " + wap.p);
1479         }
1480         return null;
1481       }
1482 
1483       Path dst = getCompletedRecoveredEditsFilePath(wap.p,
1484           regionMaximumEditLogSeqNum.get(encodedRegionName));
1485       try {
1486         if (!dst.equals(wap.p) && walFS.exists(dst)) {
1487           deleteOneWithFewerEntries(wap, dst);
1488         }
1489         // Skip the unit tests which create a splitter that reads and
1490         // writes the data without touching disk.
1491         // TestHLogSplit#testThreading is an example.
1492         if (walFS.exists(wap.p)) {
1493           if (!walFS.rename(wap.p, dst)) {
1494             throw new IOException("Failed renaming " + wap.p + " to " + dst);
1495           }
1496           LOG.info("Rename " + wap.p + " to " + dst);
1497         }
1498       } catch (IOException ioe) {
1499         LOG.error("Couldn't rename " + wap.p + " to " + dst, ioe);
1500         thrown.add(ioe);
1501         return null;
1502       }
1503       return dst;
1504     }
1505 
1506 
1507     private List<IOException> closeLogWriters(List<IOException> thrown) throws IOException {
1508       if (writersClosed) {
1509         return thrown;
1510       }
1511 
1512       if (thrown == null) {
1513         thrown = Lists.newArrayList();
1514       }
1515       try {
1516         for (WriterThread t : writerThreads) {
1517           while (t.isAlive()) {
1518             t.shouldStop = true;
1519             t.interrupt();
1520             try {
1521               t.join(10);
1522             } catch (InterruptedException e) {
1523               IOException iie = new InterruptedIOException();
1524               iie.initCause(e);
1525               throw iie;
1526             }
1527           }
1528         }
1529       } finally {
1530         WriterAndPath wap = null;
1531         for (SinkWriter tmpWAP : writers.values()) {
1532           try {
1533             wap = (WriterAndPath) tmpWAP;
1534             wap.w.close();
1535           } catch (IOException ioe) {
1536             LOG.error("Couldn't close log at " + wap.p, ioe);
1537             thrown.add(ioe);
1538             continue;
1539           }
1540           LOG.info(
1541               "Closed log " + wap.p + " (wrote " + wap.editsWritten + " edits in " + (wap.nanosSpent
1542                   / 1000 / 1000) + "ms)");
1543         }
1544         writersClosed = true;
1545       }
1546 
1547       return thrown;
1548     }
1549 
1550     /**
1551      * Get a writer and path for a log starting at the given entry. This function is threadsafe so
1552      * long as multiple threads are always acting on different regions.
1553      * @return null if this region shouldn't output any logs
1554      */
1555     WriterAndPath getWriterAndPath(Entry entry, boolean reusable) throws IOException {
1556       byte region[] = entry.getKey().getEncodedRegionName();
1557       String regionName = Bytes.toString(region);
1558       WriterAndPath ret = (WriterAndPath) writers.get(regionName);
1559       if (ret != null) {
1560         return ret;
1561       }
1562       // If we already decided that this region doesn't get any output
1563       // we don't need to check again.
1564       if (blacklistedRegions.contains(region)) {
1565         return null;
1566       }
1567       ret = createWAP(region, entry);
1568       if (ret == null) {
1569         blacklistedRegions.add(region);
1570         return null;
1571       }
1572 
1573       if(reusable) {
1574         writers.put(regionName, ret);
1575       }
1576       return ret;
1577     }
1578 
1579     /**
1580      * @return a path with a write for that path. caller should close.
1581      */
1582     WriterAndPath createWAP(byte[] region, Entry entry) throws IOException {
1583       String tmpDirName = conf.get(HConstants.TEMPORARY_FS_DIRECTORY_KEY,
1584         HConstants.DEFAULT_TEMPORARY_HDFS_DIRECTORY);
1585       Path regionedits = getRegionSplitEditsPath(entry,
1586           fileBeingSplit.getPath().getName(), tmpDirName, conf);
1587       if (regionedits == null) {
1588         return null;
1589       }
1590       if (walFS.exists(regionedits)) {
1591         LOG.warn("Found old edits file. It could be the "
1592             + "result of a previous failed split attempt. Deleting " + regionedits + ", length="
1593             + walFS.getFileStatus(regionedits).getLen());
1594         if (!walFS.delete(regionedits, false)) {
1595           LOG.warn("Failed delete of old " + regionedits);
1596         }
1597       }
1598       Writer w = createWriter(regionedits);
1599       LOG.debug("Creating writer path=" + regionedits);
1600       return new WriterAndPath(regionedits, w, entry.getKey().getLogSeqNum());
1601     }
1602 
1603     void filterCellByStore(Entry logEntry) {
1604       Map<byte[], Long> maxSeqIdInStores =
1605           regionMaxSeqIdInStores.get(Bytes.toString(logEntry.getKey().getEncodedRegionName()));
1606       if (maxSeqIdInStores == null || maxSeqIdInStores.isEmpty()) {
1607         return;
1608       }
1609       // Create the array list for the cells that aren't filtered.
1610       // We make the assumption that most cells will be kept.
1611       ArrayList<Cell> keptCells = new ArrayList<Cell>(logEntry.getEdit().getCells().size());
1612       for (Cell cell : logEntry.getEdit().getCells()) {
1613         if (CellUtil.matchingFamily(cell, WALEdit.METAFAMILY)) {
1614           keptCells.add(cell);
1615         } else {
1616           byte[] family = CellUtil.cloneFamily(cell);
1617           Long maxSeqId = maxSeqIdInStores.get(family);
1618           // Do not skip cell even if maxSeqId is null. Maybe we are in a rolling upgrade,
1619           // or the master was crashed before and we can not get the information.
1620           if (maxSeqId == null || maxSeqId.longValue() < logEntry.getKey().getLogSeqNum()) {
1621             keptCells.add(cell);
1622           }
1623         }
1624       }
1625 
1626       // Anything in the keptCells array list is still live.
1627       // So rather than removing the cells from the array list
1628       // which would be an O(n^2) operation, we just replace the list
1629       logEntry.getEdit().setCells(keptCells);
1630     }
1631 
1632     @Override
1633     public void append(RegionEntryBuffer buffer) throws IOException {
1634       appendBuffer(buffer, true);
1635     }
1636 
1637 
1638     WriterAndPath appendBuffer(RegionEntryBuffer buffer, boolean reusable) throws IOException{
1639       List<Entry> entries = buffer.entryBuffer;
1640       if (entries.isEmpty()) {
1641         LOG.warn("got an empty buffer, skipping");
1642         return null;
1643       }
1644       WriterAndPath wap = null;
1645 
1646       long startTime = System.nanoTime();
1647       try {
1648         int editsCount = 0;
1649 
1650         for (Entry logEntry : entries) {
1651           if (wap == null) {
1652             wap = getWriterAndPath(logEntry, reusable);
1653             if (wap == null) {
1654               if (LOG.isDebugEnabled()) {
1655                 LOG.debug("getWriterAndPath decided we don't need to write edits for " + logEntry);
1656               }
1657               return null;
1658             }
1659           }
1660           filterCellByStore(logEntry);
1661           if (!logEntry.getEdit().isEmpty()) {
1662             wap.w.append(logEntry);
1663             this.updateRegionMaximumEditLogSeqNum(logEntry);
1664             editsCount++;
1665           } else {
1666             wap.incrementSkippedEdits(1);
1667           }
1668         }
1669         // Pass along summary statistics
1670         wap.incrementEdits(editsCount);
1671         wap.incrementNanoTime(System.nanoTime() - startTime);
1672       } catch (IOException e) {
1673         e = RemoteExceptionHandler.checkIOException(e);
1674         LOG.fatal(" Got while writing log entry to log", e);
1675         throw e;
1676       }
1677       return wap;
1678     }
1679 
1680     @Override
1681     public boolean keepRegionEvent(Entry entry) {
1682       ArrayList<Cell> cells = entry.getEdit().getCells();
1683       for (int i = 0; i < cells.size(); i++) {
1684         if (WALEdit.isCompactionMarker(cells.get(i))) {
1685           return true;
1686         }
1687       }
1688       return false;
1689     }
1690 
1691     /**
1692      * @return a map from encoded region ID to the number of edits written out for that region.
1693      */
1694     @Override
1695     public Map<byte[], Long> getOutputCounts() {
1696       TreeMap<byte[], Long> ret = new TreeMap<byte[], Long>(Bytes.BYTES_COMPARATOR);
1697       synchronized (writers) {
1698         for (Map.Entry<String, SinkWriter> entry : writers.entrySet()) {
1699           ret.put(Bytes.toBytes(entry.getKey()), entry.getValue().editsWritten);
1700         }
1701       }
1702       return ret;
1703     }
1704 
1705     @Override
1706     public int getNumberOfRecoveredRegions() {
1707       return writers.size();
1708     }
1709   }
1710 
1711   /**
1712    * Class that will limit the number of hdfs writers we create to split the logs
1713    */
1714   class BoundedLogWriterCreationOutputSink extends LogRecoveredEditsOutputSink{
1715 
1716     ConcurrentHashMap<String, Long> regionRecoverStatMap = new ConcurrentHashMap<>();
1717 
1718 
1719     public BoundedLogWriterCreationOutputSink(PipelineController controller,
1720         EntryBuffers entryBuffers, int numWriters){
1721       super(controller, entryBuffers, numWriters);
1722     }
1723 
1724     @Override
1725     public List<Path> finishWritingAndClose() throws IOException {
1726       boolean isSuccessful;
1727       List<Path> result;
1728       try {
1729         isSuccessful = finishWriting(false);
1730       } finally {
1731         result = close();
1732       }
1733       if (isSuccessful) {
1734         splits = result;
1735       }
1736       return splits;
1737     }
1738 
1739     @Override
1740     boolean executeCloseTask(CompletionService<Void> closeCompletionService,
1741         final List<IOException> thrown, final List<Path> paths)
1742         throws InterruptedException, ExecutionException {
1743       for (final Map.Entry<byte[], RegionEntryBuffer> buffer : entryBuffers.buffers.entrySet()) {
1744         LOG.info("Submitting write then close of " +
1745             Bytes.toString(buffer.getValue().encodedRegionName));
1746         closeCompletionService.submit(new Callable<Void>() {
1747           public Void call() throws Exception {
1748             Path dst = writeThenClose(buffer.getValue());
1749             paths.add(dst);
1750             return null;
1751           }
1752         });
1753       }
1754 
1755       boolean progress_failed = false;
1756       for (int i = 0, n = entryBuffers.buffers.size(); i < n; i++) {
1757         Future<Void> future = closeCompletionService.take();
1758         future.get();
1759         if (!progress_failed && reporter != null && !reporter.progress()) {
1760           progress_failed = true;
1761         }
1762       }
1763       return progress_failed;
1764     }
1765 
1766     @Override
1767     public Map<byte[], Long> getOutputCounts() {
1768       Map<byte[], Long> regionRecoverStatMapResult = new HashMap<>();
1769       for(Map.Entry<String, Long> entry: regionRecoverStatMap.entrySet()){
1770         regionRecoverStatMapResult.put(Bytes.toBytes(entry.getKey()), entry.getValue());
1771       }
1772       return regionRecoverStatMapResult;
1773     }
1774 
1775     @Override
1776     public int getNumberOfRecoveredRegions() {
1777       return regionRecoverStatMap.size();
1778     }
1779 
1780     @Override
1781     public void append(RegionEntryBuffer buffer) throws IOException {
1782       writeThenClose(buffer);
1783     }
1784 
1785     private Path writeThenClose(RegionEntryBuffer buffer) throws IOException {
1786       WriterAndPath wap = appendBuffer(buffer, false);
1787       Path dst = null;
1788       if(wap != null){
1789         String encodedRegionName = Bytes.toString(buffer.encodedRegionName);
1790         Long value = regionRecoverStatMap.putIfAbsent(encodedRegionName, wap.editsWritten);
1791         if(value != null){
1792           Long newValue = regionRecoverStatMap.get(encodedRegionName) + wap.editsWritten;
1793           regionRecoverStatMap.put(encodedRegionName, newValue);
1794         }
1795       }
1796 
1797       List<IOException> thrown = new ArrayList<>();
1798       if(wap != null) {
1799         dst = closeWriter(Bytes.toString(buffer.encodedRegionName), wap, thrown);
1800       }
1801 
1802       if(!thrown.isEmpty()){
1803         throw MultipleIOException.createIOException(thrown);
1804       }
1805       return dst;
1806     }
1807   }
1808 
1809 
1810   /**
1811    * Class wraps the actual writer which writes data out and related statistics
1812    */
1813   public abstract static class SinkWriter {
1814     /* Count of edits written to this path */
1815     long editsWritten = 0;
1816     /* Count of edits skipped to this path */
1817     long editsSkipped = 0;
1818     /* Number of nanos spent writing to this log */
1819     long nanosSpent = 0;
1820 
1821     void incrementEdits(int edits) {
1822       editsWritten += edits;
1823     }
1824 
1825     void incrementSkippedEdits(int skipped) {
1826       editsSkipped += skipped;
1827     }
1828 
1829     void incrementNanoTime(long nanos) {
1830       nanosSpent += nanos;
1831     }
1832   }
1833 
1834   /**
1835    * Private data structure that wraps a Writer and its Path, also collecting statistics about the
1836    * data written to this output.
1837    */
1838   private final static class WriterAndPath extends SinkWriter {
1839     final Path p;
1840     final Writer w;
1841     final long minLogSeqNum;
1842 
1843     WriterAndPath(final Path p, final Writer w, final long minLogSeqNum) {
1844       this.p = p;
1845       this.w = w;
1846       this.minLogSeqNum = minLogSeqNum;
1847     }
1848   }
1849 
1850   /**
1851    * Class that manages to replay edits from WAL files directly to assigned fail over region servers
1852    */
1853   class LogReplayOutputSink extends OutputSink {
1854     private static final double BUFFER_THRESHOLD = 0.35;
1855     private static final String KEY_DELIMITER = "#";
1856 
1857     private long waitRegionOnlineTimeOut;
1858     private final Set<String> recoveredRegions = Collections.synchronizedSet(new HashSet<String>());
1859     private final Map<String, RegionServerWriter> writers =
1860         new ConcurrentHashMap<String, RegionServerWriter>();
1861     // online encoded region name -> region location map
1862     private final Map<String, HRegionLocation> onlineRegions =
1863         new ConcurrentHashMap<String, HRegionLocation>();
1864 
1865     private Map<TableName, HConnection> tableNameToHConnectionMap = Collections
1866         .synchronizedMap(new TreeMap<TableName, HConnection>());
1867     /**
1868      * Map key -> value layout
1869      * servername:table name -> Queue(Row)
1870      */
1871     private Map<String, List<Pair<HRegionLocation, Entry>>> serverToBufferQueueMap =
1872         new ConcurrentHashMap<String, List<Pair<HRegionLocation, Entry>>>();
1873     private List<Throwable> thrown = new ArrayList<Throwable>();
1874 
1875     // The following sink is used in distrubitedLogReplay mode for entries of regions in a disabling
1876     // table. It's a limitation of distributedLogReplay. Because log replay needs a region is
1877     // assigned and online before it can replay wal edits while regions of disabling/disabled table
1878     // won't be assigned by AM. We can retire this code after HBASE-8234.
1879     private LogRecoveredEditsOutputSink logRecoveredEditsOutputSink;
1880     private boolean hasEditsInDisablingOrDisabledTables = false;
1881 
1882     public LogReplayOutputSink(PipelineController controller, EntryBuffers entryBuffers,
1883         int numWriters) {
1884       super(controller, entryBuffers, numWriters);
1885       this.waitRegionOnlineTimeOut = conf.getInt(HConstants.HBASE_SPLITLOG_MANAGER_TIMEOUT,
1886           ZKSplitLogManagerCoordination.DEFAULT_TIMEOUT);
1887       this.logRecoveredEditsOutputSink = new LogRecoveredEditsOutputSink(controller,
1888         entryBuffers, numWriters);
1889       this.logRecoveredEditsOutputSink.setReporter(reporter);
1890     }
1891 
1892     @Override
1893     public void append(RegionEntryBuffer buffer) throws IOException {
1894       List<Entry> entries = buffer.entryBuffer;
1895       if (entries.isEmpty()) {
1896         LOG.warn("got an empty buffer, skipping");
1897         return;
1898       }
1899 
1900       // check if current region in a disabling or disabled table
1901       if (disablingOrDisabledTables.contains(buffer.tableName)) {
1902         // need fall back to old way
1903         logRecoveredEditsOutputSink.append(buffer);
1904         hasEditsInDisablingOrDisabledTables = true;
1905         // store regions we have recovered so far
1906         addToRecoveredRegions(Bytes.toString(buffer.encodedRegionName));
1907         return;
1908       }
1909 
1910       // group entries by region servers
1911       groupEditsByServer(entries);
1912 
1913       // process workitems
1914       String maxLocKey = null;
1915       int maxSize = 0;
1916       List<Pair<HRegionLocation, Entry>> maxQueue = null;
1917       synchronized (this.serverToBufferQueueMap) {
1918         for (Map.Entry<String, List<Pair<HRegionLocation, Entry>>> entry:
1919               serverToBufferQueueMap.entrySet()) {
1920           List<Pair<HRegionLocation, Entry>> curQueue = entry.getValue();
1921           if (curQueue.size() > maxSize) {
1922             maxSize = curQueue.size();
1923             maxQueue = curQueue;
1924             maxLocKey = entry.getKey();
1925           }
1926         }
1927         if (maxSize < minBatchSize
1928             && entryBuffers.totalBuffered < BUFFER_THRESHOLD * entryBuffers.maxHeapUsage) {
1929           // buffer more to process
1930           return;
1931         } else if (maxSize > 0) {
1932           this.serverToBufferQueueMap.remove(maxLocKey);
1933         }
1934       }
1935 
1936       if (maxSize > 0) {
1937         processWorkItems(maxLocKey, maxQueue);
1938       }
1939     }
1940 
1941     private void addToRecoveredRegions(String encodedRegionName) {
1942       if (!recoveredRegions.contains(encodedRegionName)) {
1943         recoveredRegions.add(encodedRegionName);
1944       }
1945     }
1946 
1947     /**
1948      * Helper function to group WALEntries to individual region servers
1949      * @throws IOException
1950      */
1951     private void groupEditsByServer(List<Entry> entries) throws IOException {
1952       Set<TableName> nonExistentTables = null;
1953       Long cachedLastFlushedSequenceId = -1l;
1954       for (Entry entry : entries) {
1955         WALEdit edit = entry.getEdit();
1956         TableName table = entry.getKey().getTablename();
1957         // clear scopes which isn't needed for recovery
1958         entry.getKey().setScopes(null);
1959         String encodeRegionNameStr = Bytes.toString(entry.getKey().getEncodedRegionName());
1960         // skip edits of non-existent tables
1961         if (nonExistentTables != null && nonExistentTables.contains(table)) {
1962           this.skippedEdits.incrementAndGet();
1963           continue;
1964         }
1965 
1966         Map<byte[], Long> maxStoreSequenceIds = null;
1967         boolean needSkip = false;
1968         HRegionLocation loc = null;
1969         String locKey = null;
1970         List<Cell> cells = edit.getCells();
1971         List<Cell> skippedCells = new ArrayList<Cell>();
1972         HConnection hconn = this.getConnectionByTableName(table);
1973 
1974         for (Cell cell : cells) {
1975           byte[] row = CellUtil.cloneRow(cell);
1976           byte[] family = CellUtil.cloneFamily(cell);
1977           boolean isCompactionEntry = false;
1978           if (CellUtil.matchingFamily(cell, WALEdit.METAFAMILY)) {
1979             CompactionDescriptor compaction = WALEdit.getCompaction(cell);
1980             if (compaction != null && compaction.hasRegionName()) {
1981               try {
1982                 byte[][] regionName = HRegionInfo.parseRegionName(compaction.getRegionName()
1983                   .toByteArray());
1984                 row = regionName[1]; // startKey of the region
1985                 family = compaction.getFamilyName().toByteArray();
1986                 isCompactionEntry = true;
1987               } catch (Exception ex) {
1988                 LOG.warn("Unexpected exception received, ignoring " + ex);
1989                 skippedCells.add(cell);
1990                 continue;
1991               }
1992             } else {
1993               skippedCells.add(cell);
1994               continue;
1995             }
1996           }
1997 
1998           try {
1999             loc =
2000                 locateRegionAndRefreshLastFlushedSequenceId(hconn, table, row,
2001                   encodeRegionNameStr);
2002             // skip replaying the compaction if the region is gone
2003             if (isCompactionEntry && !encodeRegionNameStr.equalsIgnoreCase(
2004               loc.getRegionInfo().getEncodedName())) {
2005               LOG.info("Not replaying a compaction marker for an older region: "
2006                   + encodeRegionNameStr);
2007               needSkip = true;
2008             }
2009           } catch (TableNotFoundException ex) {
2010             // table has been deleted so skip edits of the table
2011             LOG.info("Table " + table + " doesn't exist. Skip log replay for region "
2012                 + encodeRegionNameStr);
2013             lastFlushedSequenceIds.put(encodeRegionNameStr, Long.MAX_VALUE);
2014             if (nonExistentTables == null) {
2015               nonExistentTables = new TreeSet<TableName>();
2016             }
2017             nonExistentTables.add(table);
2018             this.skippedEdits.incrementAndGet();
2019             needSkip = true;
2020             break;
2021           }
2022 
2023           cachedLastFlushedSequenceId =
2024               lastFlushedSequenceIds.get(loc.getRegionInfo().getEncodedName());
2025           if (cachedLastFlushedSequenceId != null
2026               && cachedLastFlushedSequenceId >= entry.getKey().getLogSeqNum()) {
2027             // skip the whole WAL entry
2028             this.skippedEdits.incrementAndGet();
2029             needSkip = true;
2030             break;
2031           } else {
2032             if (maxStoreSequenceIds == null) {
2033               maxStoreSequenceIds =
2034                   regionMaxSeqIdInStores.get(loc.getRegionInfo().getEncodedName());
2035             }
2036             if (maxStoreSequenceIds != null) {
2037               Long maxStoreSeqId = maxStoreSequenceIds.get(family);
2038               if (maxStoreSeqId == null || maxStoreSeqId >= entry.getKey().getLogSeqNum()) {
2039                 // skip current kv if column family doesn't exist anymore or already flushed
2040                 skippedCells.add(cell);
2041                 continue;
2042               }
2043             }
2044           }
2045         }
2046 
2047         // skip the edit
2048         if (loc == null || needSkip) continue;
2049 
2050         if (!skippedCells.isEmpty()) {
2051           cells.removeAll(skippedCells);
2052         }
2053 
2054         synchronized (serverToBufferQueueMap) {
2055           locKey = loc.getHostnamePort() + KEY_DELIMITER + table;
2056           List<Pair<HRegionLocation, Entry>> queue = serverToBufferQueueMap.get(locKey);
2057           if (queue == null) {
2058             queue =
2059                 Collections.synchronizedList(new ArrayList<Pair<HRegionLocation, Entry>>());
2060             serverToBufferQueueMap.put(locKey, queue);
2061           }
2062           queue.add(new Pair<HRegionLocation, Entry>(loc, entry));
2063         }
2064         // store regions we have recovered so far
2065         addToRecoveredRegions(loc.getRegionInfo().getEncodedName());
2066       }
2067     }
2068 
2069     /**
2070      * Locate destination region based on table name & row. This function also makes sure the
2071      * destination region is online for replay.
2072      * @throws IOException
2073      */
2074     private HRegionLocation locateRegionAndRefreshLastFlushedSequenceId(HConnection hconn,
2075         TableName table, byte[] row, String originalEncodedRegionName) throws IOException {
2076       // fetch location from cache
2077       HRegionLocation loc = onlineRegions.get(originalEncodedRegionName);
2078       if(loc != null) return loc;
2079       // fetch location from hbase:meta directly without using cache to avoid hit old dead server
2080       loc = hconn.getRegionLocation(table, row, true);
2081       if (loc == null) {
2082         throw new IOException("Can't locate location for row:" + Bytes.toString(row)
2083             + " of table:" + table);
2084       }
2085       // check if current row moves to a different region due to region merge/split
2086       if (!originalEncodedRegionName.equalsIgnoreCase(loc.getRegionInfo().getEncodedName())) {
2087         // originalEncodedRegionName should have already flushed
2088         lastFlushedSequenceIds.put(originalEncodedRegionName, Long.MAX_VALUE);
2089         HRegionLocation tmpLoc = onlineRegions.get(loc.getRegionInfo().getEncodedName());
2090         if (tmpLoc != null) return tmpLoc;
2091       }
2092 
2093       Long lastFlushedSequenceId = -1l;
2094       AtomicBoolean isRecovering = new AtomicBoolean(true);
2095       loc = waitUntilRegionOnline(loc, row, this.waitRegionOnlineTimeOut, isRecovering);
2096       if (!isRecovering.get()) {
2097         // region isn't in recovering at all because WAL file may contain a region that has
2098         // been moved to somewhere before hosting RS fails
2099         lastFlushedSequenceIds.put(loc.getRegionInfo().getEncodedName(), Long.MAX_VALUE);
2100         LOG.info("logReplay skip region: " + loc.getRegionInfo().getEncodedName()
2101             + " because it's not in recovering.");
2102       } else {
2103         Long cachedLastFlushedSequenceId =
2104             lastFlushedSequenceIds.get(loc.getRegionInfo().getEncodedName());
2105 
2106         // retrieve last flushed sequence Id from ZK. Because region postOpenDeployTasks will
2107         // update the value for the region
2108         RegionStoreSequenceIds ids =
2109             csm.getSplitLogWorkerCoordination().getRegionFlushedSequenceId(failedServerName,
2110               loc.getRegionInfo().getEncodedName());
2111         if (ids != null) {
2112           lastFlushedSequenceId = ids.getLastFlushedSequenceId();
2113           Map<byte[], Long> storeIds = new TreeMap<byte[], Long>(Bytes.BYTES_COMPARATOR);
2114           List<StoreSequenceId> maxSeqIdInStores = ids.getStoreSequenceIdList();
2115           for (StoreSequenceId id : maxSeqIdInStores) {
2116             storeIds.put(id.getFamilyName().toByteArray(), id.getSequenceId());
2117           }
2118           regionMaxSeqIdInStores.put(loc.getRegionInfo().getEncodedName(), storeIds);
2119         }
2120 
2121         if (cachedLastFlushedSequenceId == null
2122             || lastFlushedSequenceId > cachedLastFlushedSequenceId) {
2123           lastFlushedSequenceIds.put(loc.getRegionInfo().getEncodedName(), lastFlushedSequenceId);
2124         }
2125       }
2126 
2127       onlineRegions.put(loc.getRegionInfo().getEncodedName(), loc);
2128       return loc;
2129     }
2130 
2131     private void processWorkItems(String key, List<Pair<HRegionLocation, Entry>> actions)
2132         throws IOException {
2133       RegionServerWriter rsw = null;
2134 
2135       long startTime = System.nanoTime();
2136       try {
2137         rsw = getRegionServerWriter(key);
2138         rsw.sink.replayEntries(actions);
2139 
2140         // Pass along summary statistics
2141         rsw.incrementEdits(actions.size());
2142         rsw.incrementNanoTime(System.nanoTime() - startTime);
2143       } catch (IOException e) {
2144         e = RemoteExceptionHandler.checkIOException(e);
2145         LOG.fatal(" Got while writing log entry to log", e);
2146         throw e;
2147       }
2148     }
2149 
2150     /**
2151      * Wait until region is online on the destination region server
2152      * @param loc
2153      * @param row
2154      * @param timeout How long to wait
2155      * @param isRecovering Recovering state of the region interested on destination region server.
2156      * @return True when region is online on the destination region server
2157      * @throws InterruptedException
2158      */
2159     private HRegionLocation waitUntilRegionOnline(HRegionLocation loc, byte[] row,
2160         final long timeout, AtomicBoolean isRecovering)
2161         throws IOException {
2162       final long endTime = EnvironmentEdgeManager.currentTime() + timeout;
2163       final long pause = conf.getLong(HConstants.HBASE_CLIENT_PAUSE,
2164         HConstants.DEFAULT_HBASE_CLIENT_PAUSE);
2165       boolean reloadLocation = false;
2166       TableName tableName = loc.getRegionInfo().getTable();
2167       int tries = 0;
2168       Throwable cause = null;
2169       while (endTime > EnvironmentEdgeManager.currentTime()) {
2170         try {
2171           // Try and get regioninfo from the hosting server.
2172           HConnection hconn = getConnectionByTableName(tableName);
2173           if(reloadLocation) {
2174             loc = hconn.getRegionLocation(tableName, row, true);
2175           }
2176           BlockingInterface remoteSvr = hconn.getAdmin(loc.getServerName());
2177           HRegionInfo region = loc.getRegionInfo();
2178           try {
2179             GetRegionInfoRequest request =
2180                 RequestConverter.buildGetRegionInfoRequest(region.getRegionName());
2181             GetRegionInfoResponse response = remoteSvr.getRegionInfo(null, request);
2182             if (HRegionInfo.convert(response.getRegionInfo()) != null) {
2183               isRecovering.set((response.hasIsRecovering()) ? response.getIsRecovering() : true);
2184               return loc;
2185             }
2186           } catch (ServiceException se) {
2187             throw ProtobufUtil.getRemoteException(se);
2188           }
2189         } catch (IOException e) {
2190           cause = e.getCause();
2191           if(!(cause instanceof RegionOpeningException)) {
2192             reloadLocation = true;
2193           }
2194         }
2195         long expectedSleep = ConnectionUtils.getPauseTime(pause, tries);
2196         try {
2197           Thread.sleep(expectedSleep);
2198         } catch (InterruptedException e) {
2199           throw new IOException("Interrupted when waiting region " +
2200               loc.getRegionInfo().getEncodedName() + " online.", e);
2201         }
2202         tries++;
2203       }
2204 
2205       throw new IOException("Timeout when waiting region " + loc.getRegionInfo().getEncodedName() +
2206         " online for " + timeout + " milliseconds.", cause);
2207     }
2208 
2209     @Override
2210     public boolean flush() throws IOException {
2211       String curLoc = null;
2212       int curSize = 0;
2213       List<Pair<HRegionLocation, Entry>> curQueue = null;
2214       synchronized (this.serverToBufferQueueMap) {
2215         for (Map.Entry<String, List<Pair<HRegionLocation, Entry>>> entry :
2216                 serverToBufferQueueMap.entrySet()) {
2217           String locationKey = entry.getKey();
2218           curQueue = entry.getValue();
2219           if (!curQueue.isEmpty()) {
2220             curSize = curQueue.size();
2221             curLoc = locationKey;
2222             break;
2223           }
2224         }
2225         if (curSize > 0) {
2226           this.serverToBufferQueueMap.remove(curLoc);
2227         }
2228       }
2229 
2230       if (curSize > 0) {
2231         this.processWorkItems(curLoc, curQueue);
2232         // We should already have control of the monitor; ensure this is the case.
2233         synchronized(controller.dataAvailable) {
2234           controller.dataAvailable.notifyAll();
2235         }
2236         return true;
2237       }
2238       return false;
2239     }
2240 
2241     @Override
2242     public boolean keepRegionEvent(Entry entry) {
2243       return true;
2244     }
2245 
2246     void addWriterError(Throwable t) {
2247       thrown.add(t);
2248     }
2249 
2250     @Override
2251     public List<Path> finishWritingAndClose() throws IOException {
2252       try {
2253         if (!finishWriting(false)) {
2254           return null;
2255         }
2256         if (hasEditsInDisablingOrDisabledTables) {
2257           splits = logRecoveredEditsOutputSink.finishWritingAndClose();
2258         } else {
2259           splits = new ArrayList<Path>();
2260         }
2261         // returns an empty array in order to keep interface same as old way
2262         return splits;
2263       } finally {
2264         List<IOException> thrown = closeRegionServerWriters();
2265         if (thrown != null && !thrown.isEmpty()) {
2266           throw MultipleIOException.createIOException(thrown);
2267         }
2268       }
2269     }
2270 
2271     @Override
2272     int getNumOpenWriters() {
2273       return this.writers.size() + this.logRecoveredEditsOutputSink.getNumOpenWriters();
2274     }
2275 
2276     private List<IOException> closeRegionServerWriters() throws IOException {
2277       List<IOException> result = null;
2278       if (!writersClosed) {
2279         result = Lists.newArrayList();
2280         try {
2281           for (WriterThread t : writerThreads) {
2282             while (t.isAlive()) {
2283               t.shouldStop = true;
2284               t.interrupt();
2285               try {
2286                 t.join(10);
2287               } catch (InterruptedException e) {
2288                 IOException iie = new InterruptedIOException();
2289                 iie.initCause(e);
2290                 throw iie;
2291               }
2292             }
2293           }
2294         } finally {
2295           synchronized (writers) {
2296             for (Map.Entry<String, RegionServerWriter> entry : writers.entrySet()) {
2297               String locationKey = entry.getKey();
2298               RegionServerWriter tmpW = entry.getValue();
2299               try {
2300                 tmpW.close();
2301               } catch (IOException ioe) {
2302                 LOG.error("Couldn't close writer for region server:" + locationKey, ioe);
2303                 result.add(ioe);
2304               }
2305             }
2306           }
2307 
2308           // close connections
2309           synchronized (this.tableNameToHConnectionMap) {
2310             for (Map.Entry<TableName, HConnection> entry :
2311                   tableNameToHConnectionMap.entrySet()) {
2312               TableName tableName = entry.getKey();
2313               HConnection hconn = entry.getValue();
2314               try {
2315                 hconn.clearRegionCache();
2316                 hconn.close();
2317               } catch (IOException ioe) {
2318                 result.add(ioe);
2319               }
2320             }
2321           }
2322           writersClosed = true;
2323         }
2324       }
2325       return result;
2326     }
2327 
2328     @Override
2329     public Map<byte[], Long> getOutputCounts() {
2330       TreeMap<byte[], Long> ret = new TreeMap<byte[], Long>(Bytes.BYTES_COMPARATOR);
2331       synchronized (writers) {
2332         for (Map.Entry<String, RegionServerWriter> entry : writers.entrySet()) {
2333           ret.put(Bytes.toBytes(entry.getKey()), entry.getValue().editsWritten);
2334         }
2335       }
2336       return ret;
2337     }
2338 
2339     @Override
2340     public int getNumberOfRecoveredRegions() {
2341       return this.recoveredRegions.size();
2342     }
2343 
2344     /**
2345      * Get a writer and path for a log starting at the given entry. This function is threadsafe so
2346      * long as multiple threads are always acting on different regions.
2347      * @return null if this region shouldn't output any logs
2348      */
2349     private RegionServerWriter getRegionServerWriter(String loc) throws IOException {
2350       RegionServerWriter ret = writers.get(loc);
2351       if (ret != null) {
2352         return ret;
2353       }
2354 
2355       TableName tableName = getTableFromLocationStr(loc);
2356       if(tableName == null){
2357         throw new IOException("Invalid location string:" + loc + " found. Replay aborted.");
2358       }
2359 
2360       HConnection hconn = getConnectionByTableName(tableName);
2361       synchronized (writers) {
2362         ret = writers.get(loc);
2363         if (ret == null) {
2364           ret = new RegionServerWriter(conf, tableName, hconn);
2365           writers.put(loc, ret);
2366         }
2367       }
2368       return ret;
2369     }
2370 
2371     private HConnection getConnectionByTableName(final TableName tableName) throws IOException {
2372       HConnection hconn = this.tableNameToHConnectionMap.get(tableName);
2373       if (hconn == null) {
2374         synchronized (this.tableNameToHConnectionMap) {
2375           hconn = this.tableNameToHConnectionMap.get(tableName);
2376           if (hconn == null) {
2377             hconn = HConnectionManager.getConnection(conf);
2378             this.tableNameToHConnectionMap.put(tableName, hconn);
2379           }
2380         }
2381       }
2382       return hconn;
2383     }
2384     private TableName getTableFromLocationStr(String loc) {
2385       /**
2386        * location key is in format <server name:port>#<table name>
2387        */
2388       String[] splits = loc.split(KEY_DELIMITER);
2389       if (splits.length != 2) {
2390         return null;
2391       }
2392       return TableName.valueOf(splits[1]);
2393     }
2394   }
2395 
2396   /**
2397    * Private data structure that wraps a receiving RS and collecting statistics about the data
2398    * written to this newly assigned RS.
2399    */
2400   private final static class RegionServerWriter extends SinkWriter {
2401     final WALEditsReplaySink sink;
2402 
2403     RegionServerWriter(final Configuration conf, final TableName tableName, final HConnection conn)
2404         throws IOException {
2405       this.sink = new WALEditsReplaySink(conf, tableName, conn);
2406     }
2407 
2408     void close() throws IOException {
2409     }
2410   }
2411 
2412   static class CorruptedLogFileException extends Exception {
2413     private static final long serialVersionUID = 1L;
2414 
2415     CorruptedLogFileException(String s) {
2416       super(s);
2417     }
2418   }
2419 
2420   /** A struct used by getMutationsFromWALEntry */
2421   public static class MutationReplay implements Comparable<MutationReplay> {
2422     public MutationReplay(MutationType type, Mutation mutation, long nonceGroup, long nonce) {
2423       this.type = type;
2424       this.mutation = mutation;
2425       if(this.mutation.getDurability() != Durability.SKIP_WAL) {
2426         // using ASYNC_WAL for relay
2427         this.mutation.setDurability(Durability.ASYNC_WAL);
2428       }
2429       this.nonceGroup = nonceGroup;
2430       this.nonce = nonce;
2431     }
2432 
2433     public final MutationType type;
2434     public final Mutation mutation;
2435     public final long nonceGroup;
2436     public final long nonce;
2437 
2438     @Override
2439     public int compareTo(final MutationReplay d) {
2440       return this.mutation.compareTo(d.mutation);
2441     }
2442 
2443     @Override
2444     public boolean equals(Object obj) {
2445       if(!(obj instanceof MutationReplay)) {
2446         return false;
2447       } else {
2448         return this.compareTo((MutationReplay)obj) == 0;
2449       }
2450     }
2451 
2452     @Override
2453     public int hashCode() {
2454       return this.mutation.hashCode();
2455     }
2456   }
2457 
2458   /**
2459    * This function is used to construct mutations from a WALEntry. It also
2460    * reconstructs WALKey &amp; WALEdit from the passed in WALEntry
2461    * @param entry
2462    * @param cells
2463    * @param logEntry pair of WALKey and WALEdit instance stores WALKey and WALEdit instances
2464    *          extracted from the passed in WALEntry.
2465    * @param durability
2466    * @return list of Pair&lt;MutationType, Mutation&gt; to be replayed
2467    * @throws IOException
2468    */
2469   public static List<MutationReplay> getMutationsFromWALEntry(WALEntry entry, CellScanner cells,
2470       Pair<WALKey, WALEdit> logEntry, Durability durability)
2471           throws IOException {
2472     if (entry == null) {
2473       // return an empty array
2474       return new ArrayList<MutationReplay>();
2475     }
2476 
2477     long replaySeqId = (entry.getKey().hasOrigSequenceNumber()) ?
2478       entry.getKey().getOrigSequenceNumber() : entry.getKey().getLogSequenceNumber();
2479     int count = entry.getAssociatedCellCount();
2480     List<MutationReplay> mutations = new ArrayList<MutationReplay>();
2481     Cell previousCell = null;
2482     Mutation m = null;
2483     WALKey key = null;
2484     WALEdit val = null;
2485     if (logEntry != null) val = new WALEdit();
2486 
2487     for (int i = 0; i < count; i++) {
2488       // Throw index out of bounds if our cell count is off
2489       if (!cells.advance()) {
2490         throw new ArrayIndexOutOfBoundsException("Expected=" + count + ", index=" + i);
2491       }
2492       Cell cell = cells.current();
2493       if (val != null) val.add(cell);
2494 
2495       boolean isNewRowOrType =
2496           previousCell == null || previousCell.getTypeByte() != cell.getTypeByte()
2497               || !CellUtil.matchingRow(previousCell, cell);
2498       if (isNewRowOrType) {
2499         // Create new mutation
2500         if (CellUtil.isDelete(cell)) {
2501           m = new Delete(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
2502           // Deletes don't have nonces.
2503           mutations.add(new MutationReplay(
2504               MutationType.DELETE, m, HConstants.NO_NONCE, HConstants.NO_NONCE));
2505         } else {
2506           m = new Put(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
2507           // Puts might come from increment or append, thus we need nonces.
2508           long nonceGroup = entry.getKey().hasNonceGroup()
2509               ? entry.getKey().getNonceGroup() : HConstants.NO_NONCE;
2510           long nonce = entry.getKey().hasNonce() ? entry.getKey().getNonce() : HConstants.NO_NONCE;
2511           mutations.add(new MutationReplay(MutationType.PUT, m, nonceGroup, nonce));
2512         }
2513       }
2514       if (CellUtil.isDelete(cell)) {
2515         ((Delete) m).addDeleteMarker(cell);
2516       } else {
2517         ((Put) m).add(cell);
2518       }
2519       m.setDurability(durability);
2520       previousCell = cell;
2521     }
2522 
2523     // reconstruct WALKey
2524     if (logEntry != null) {
2525       org.apache.hadoop.hbase.protobuf.generated.WALProtos.WALKey walKeyProto = entry.getKey();
2526       List<UUID> clusterIds = new ArrayList<UUID>(walKeyProto.getClusterIdsCount());
2527       for (HBaseProtos.UUID uuid : entry.getKey().getClusterIdsList()) {
2528         clusterIds.add(new UUID(uuid.getMostSigBits(), uuid.getLeastSigBits()));
2529       }
2530       // we use HLogKey here instead of WALKey directly to support legacy coprocessors.
2531       key = new HLogKey(walKeyProto.getEncodedRegionName().toByteArray(), TableName.valueOf(
2532               walKeyProto.getTableName().toByteArray()), replaySeqId, walKeyProto.getWriteTime(),
2533               clusterIds, walKeyProto.getNonceGroup(), walKeyProto.getNonce(), null);
2534       logEntry.setFirst(key);
2535       logEntry.setSecond(val);
2536     }
2537 
2538     return mutations;
2539   }
2540 }