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.replication.regionserver;
20  
21  import java.io.EOFException;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Comparator;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.UUID;
31  import java.util.concurrent.ConcurrentHashMap;
32  import java.util.concurrent.PriorityBlockingQueue;
33  import java.util.concurrent.TimeUnit;
34  import java.util.concurrent.atomic.AtomicLong;
35  
36  import org.apache.commons.lang.StringUtils;
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.apache.hadoop.conf.Configuration;
40  import org.apache.hadoop.fs.FileStatus;
41  import org.apache.hadoop.fs.FileSystem;
42  import org.apache.hadoop.fs.Path;
43  import org.apache.hadoop.hbase.Cell;
44  import org.apache.hadoop.hbase.CellUtil;
45  import org.apache.hadoop.hbase.HBaseConfiguration;
46  import org.apache.hadoop.hbase.HConstants;
47  import org.apache.hadoop.hbase.Stoppable;
48  import org.apache.hadoop.hbase.TableName;
49  import org.apache.hadoop.hbase.classification.InterfaceAudience;
50  import org.apache.hadoop.hbase.protobuf.generated.WALProtos.BulkLoadDescriptor;
51  import org.apache.hadoop.hbase.protobuf.generated.WALProtos.StoreDescriptor;
52  import org.apache.hadoop.hbase.regionserver.RSRpcServices;
53  import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
54  import org.apache.hadoop.hbase.replication.ChainWALEntryFilter;
55  import org.apache.hadoop.hbase.replication.ReplicationEndpoint;
56  import org.apache.hadoop.hbase.replication.ReplicationException;
57  import org.apache.hadoop.hbase.replication.ReplicationPeers;
58  import org.apache.hadoop.hbase.replication.ReplicationQueueInfo;
59  import org.apache.hadoop.hbase.replication.ReplicationQueues;
60  import org.apache.hadoop.hbase.replication.SystemTableWALEntryFilter;
61  import org.apache.hadoop.hbase.replication.WALEntryFilter;
62  import org.apache.hadoop.hbase.util.Bytes;
63  import org.apache.hadoop.hbase.util.CancelableProgressable;
64  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
65  import org.apache.hadoop.hbase.util.FSUtils;
66  import org.apache.hadoop.hbase.util.LeaseNotRecoveredException;
67  import org.apache.hadoop.hbase.util.Threads;
68  import org.apache.hadoop.hbase.wal.DefaultWALProvider;
69  import org.apache.hadoop.hbase.wal.WAL;
70  import org.apache.hadoop.hbase.wal.WALKey;
71  
72  import com.google.common.collect.Lists;
73  import com.google.common.util.concurrent.ListenableFuture;
74  import com.google.common.util.concurrent.Service;
75  
76  /**
77   * Class that handles the source of a replication stream.
78   * Currently does not handle more than 1 slave
79   * For each slave cluster it selects a random number of peers
80   * using a replication ratio. For example, if replication ration = 0.1
81   * and slave cluster has 100 region servers, 10 will be selected.
82   * <p>
83   * A stream is considered down when we cannot contact a region server on the
84   * peer cluster for more than 55 seconds by default.
85   * </p>
86   *
87   */
88  @InterfaceAudience.Private
89  public class ReplicationSource extends Thread
90      implements ReplicationSourceInterface {
91  
92    private static final Log LOG = LogFactory.getLog(ReplicationSource.class);
93    // Queues of logs to process, entry in format of walGroupId->queue,
94    // each presents a queue for one wal group
95    private Map<String, PriorityBlockingQueue<Path>> queues =
96        new HashMap<String, PriorityBlockingQueue<Path>>();
97    // per group queue size, keep no more than this number of logs in each wal group
98    private int queueSizePerGroup;
99    private ReplicationQueues replicationQueues;
100   private ReplicationPeers replicationPeers;
101 
102   private Configuration conf;
103   private ReplicationQueueInfo replicationQueueInfo;
104   // id of the peer cluster this source replicates to
105   private String peerId;
106   // The manager of all sources to which we ping back our progress
107   private ReplicationSourceManager manager;
108   // Should we stop everything?
109   private Stoppable stopper;
110   // How long should we sleep for each retry
111   private long sleepForRetries;
112   // Max size in bytes of entriesArray
113   private long replicationQueueSizeCapacity;
114   // Max number of entries in entriesArray
115   private int replicationQueueNbCapacity;
116   private FileSystem fs;
117   // id of this cluster
118   private UUID clusterId;
119   // id of the other cluster
120   private UUID peerClusterId;
121   // total number of edits we replicated
122   private AtomicLong totalReplicatedEdits = new AtomicLong(0);
123   // total number of edits we replicated
124   private AtomicLong totalReplicatedOperations = new AtomicLong(0);
125   // The znode we currently play with
126   private String peerClusterZnode;
127   // Maximum number of retries before taking bold actions
128   private int maxRetriesMultiplier;
129   // Indicates if this particular source is running
130   private volatile boolean sourceRunning = false;
131   // Metrics for this source
132   private MetricsSource metrics;
133   //WARN threshold for the number of queued logs, defaults to 2
134   private int logQueueWarnThreshold;
135   // ReplicationEndpoint which will handle the actual replication
136   private ReplicationEndpoint replicationEndpoint;
137   // A filter (or a chain of filters) for the WAL entries.
138   private WALEntryFilter walEntryFilter;
139   // throttler
140   private ReplicationThrottler throttler;
141   private ConcurrentHashMap<String, ReplicationSourceWorkerThread> workerThreads =
142       new ConcurrentHashMap<String, ReplicationSourceWorkerThread>();
143   // Hold the state of a replication worker thread
144   public enum WorkerState {
145     RUNNING,
146     STOPPED,
147     FINISHED  // The worker is done processing a recovered queue
148   }
149 
150   /**
151    * Instantiation method used by region servers
152    *
153    * @param conf configuration to use
154    * @param fs file system to use
155    * @param manager replication manager to ping to
156    * @param stopper     the atomic boolean to use to stop the regionserver
157    * @param peerClusterZnode the name of our znode
158    * @param clusterId unique UUID for the cluster
159    * @param replicationEndpoint the replication endpoint implementation
160    * @param metrics metrics for replication source
161    * @throws IOException
162    */
163   @Override
164   public void init(final Configuration conf, final FileSystem fs,
165       final ReplicationSourceManager manager, final ReplicationQueues replicationQueues,
166       final ReplicationPeers replicationPeers, final Stoppable stopper,
167       final String peerClusterZnode, final UUID clusterId, ReplicationEndpoint replicationEndpoint,
168       final MetricsSource metrics)
169           throws IOException {
170     this.stopper = stopper;
171     this.conf = HBaseConfiguration.create(conf);
172     decorateConf();
173     this.replicationQueueSizeCapacity =
174         this.conf.getLong("replication.source.size.capacity", 1024*1024*64);
175     this.replicationQueueNbCapacity =
176         this.conf.getInt("replication.source.nb.capacity", 25000);
177     this.sleepForRetries =
178         this.conf.getLong("replication.source.sleepforretries", 1000);    // 1 second
179     this.maxRetriesMultiplier =
180         this.conf.getInt("replication.source.maxretriesmultiplier", 300); // 5 minutes @ 1 sec per
181     this.queueSizePerGroup = this.conf.getInt("hbase.regionserver.maxlogs", 32);
182     long bandwidth = this.conf.getLong("replication.source.per.peer.node.bandwidth", 0);
183     this.throttler = new ReplicationThrottler((double)bandwidth/10.0);
184     this.replicationQueues = replicationQueues;
185     this.replicationPeers = replicationPeers;
186     this.manager = manager;
187     this.fs = fs;
188     this.metrics = metrics;
189     this.clusterId = clusterId;
190 
191     this.peerClusterZnode = peerClusterZnode;
192     this.replicationQueueInfo = new ReplicationQueueInfo(peerClusterZnode);
193     // ReplicationQueueInfo parses the peerId out of the znode for us
194     this.peerId = this.replicationQueueInfo.getPeerId();
195     this.logQueueWarnThreshold = this.conf.getInt("replication.source.log.queue.warn", 2);
196     this.replicationEndpoint = replicationEndpoint;
197   }
198 
199   private void decorateConf() {
200     String replicationCodec = this.conf.get(HConstants.REPLICATION_CODEC_CONF_KEY);
201     if (StringUtils.isNotEmpty(replicationCodec)) {
202       this.conf.set(HConstants.RPC_CODEC_CONF_KEY, replicationCodec);
203     }
204   }
205 
206   @Override
207   public void enqueueLog(Path log) {
208     String logPrefix = DefaultWALProvider.getWALPrefixFromWALName(log.getName());
209     PriorityBlockingQueue<Path> queue = queues.get(logPrefix);
210     if (queue == null) {
211       queue = new PriorityBlockingQueue<Path>(queueSizePerGroup, new LogsComparator());
212       queues.put(logPrefix, queue);
213       if (this.sourceRunning) {
214         // new wal group observed after source startup, start a new worker thread to track it
215         // notice: it's possible that log enqueued when this.running is set but worker thread
216         // still not launched, so it's necessary to check workerThreads before start the worker
217         final ReplicationSourceWorkerThread worker =
218             new ReplicationSourceWorkerThread(logPrefix, queue, replicationQueueInfo, this);
219         ReplicationSourceWorkerThread extant = workerThreads.putIfAbsent(logPrefix, worker);
220         if (extant != null) {
221           LOG.debug("Someone has beat us to start a worker thread for wal group " + logPrefix);
222         } else {
223           LOG.debug("Starting up worker for wal group " + logPrefix);
224           worker.startup();
225         }
226       }
227     }
228     queue.put(log);
229     this.metrics.incrSizeOfLogQueue();
230     // This will log a warning for each new log that gets created above the warn threshold
231     int queueSize = queue.size();
232     if (queueSize > this.logQueueWarnThreshold) {
233       LOG.warn("WAL group " + logPrefix + " queue size: " + queueSize
234           + " exceeds value of replication.source.log.queue.warn: " + logQueueWarnThreshold);
235     }
236   }
237 
238   @Override
239   public void addHFileRefs(TableName tableName, byte[] family, List<String> files)
240       throws ReplicationException {
241     String peerId = peerClusterZnode;
242     if (peerId.contains("-")) {
243       // peerClusterZnode will be in the form peerId + "-" + rsZNode.
244       // A peerId will not have "-" in its name, see HBASE-11394
245       peerId = peerClusterZnode.split("-")[0];
246     }
247     Map<TableName, List<String>> tableCFMap = replicationPeers.getPeer(peerId).getTableCFs();
248     if (tableCFMap != null) {
249       List<String> tableCfs = tableCFMap.get(tableName);
250       if (tableCFMap.containsKey(tableName)
251           && (tableCfs == null || tableCfs.contains(Bytes.toString(family)))) {
252         this.replicationQueues.addHFileRefs(peerId, files);
253         metrics.incrSizeOfHFileRefsQueue(files.size());
254       } else {
255         LOG.debug("HFiles will not be replicated belonging to the table " + tableName + " family "
256             + Bytes.toString(family) + " to peer id " + peerId);
257       }
258     } else {
259       // user has explicitly not defined any table cfs for replication, means replicate all the
260       // data
261       this.replicationQueues.addHFileRefs(peerId, files);
262       metrics.incrSizeOfHFileRefsQueue(files.size());
263     }
264   }
265 
266   private void uninitialize() {
267     LOG.debug("Source exiting " + this.peerId);
268     metrics.clear();
269     if (replicationEndpoint.state() == Service.State.STARTING
270         || replicationEndpoint.state() == Service.State.RUNNING) {
271       replicationEndpoint.stopAndWait();
272     }
273   }
274 
275   @Override
276   public void run() {
277     // mark we are running now
278     this.sourceRunning = true;
279     try {
280       // start the endpoint, connect to the cluster
281       Service.State state = replicationEndpoint.start().get();
282       if (state != Service.State.RUNNING) {
283         LOG.warn("ReplicationEndpoint was not started. Exiting");
284         uninitialize();
285         return;
286       }
287     } catch (Exception ex) {
288       LOG.warn("Error starting ReplicationEndpoint, exiting", ex);
289       throw new RuntimeException(ex);
290     }
291 
292     // get the WALEntryFilter from ReplicationEndpoint and add it to default filters
293     ArrayList<WALEntryFilter> filters = Lists.newArrayList(
294       (WALEntryFilter)new SystemTableWALEntryFilter());
295     WALEntryFilter filterFromEndpoint = this.replicationEndpoint.getWALEntryfilter();
296     if (filterFromEndpoint != null) {
297       filters.add(filterFromEndpoint);
298     }
299     this.walEntryFilter = new ChainWALEntryFilter(filters);
300 
301     int sleepMultiplier = 1;
302     // delay this until we are in an asynchronous thread
303     while (this.isSourceActive() && this.peerClusterId == null) {
304       this.peerClusterId = replicationEndpoint.getPeerUUID();
305       if (this.isSourceActive() && this.peerClusterId == null) {
306         if (sleepForRetries("Cannot contact the peer's zk ensemble", sleepMultiplier)) {
307           sleepMultiplier++;
308         }
309       }
310     }
311 
312     // In rare case, zookeeper setting may be messed up. That leads to the incorrect
313     // peerClusterId value, which is the same as the source clusterId
314     if (clusterId.equals(peerClusterId) && !replicationEndpoint.canReplicateToSameCluster()) {
315       this.terminate("ClusterId " + clusterId + " is replicating to itself: peerClusterId "
316           + peerClusterId + " which is not allowed by ReplicationEndpoint:"
317           + replicationEndpoint.getClass().getName(), null, false);
318       this.manager.closeQueue(this);
319       return;
320     }
321     LOG.info("Replicating " + clusterId + " -> " + peerClusterId);
322     // start workers
323     for (Map.Entry<String, PriorityBlockingQueue<Path>> entry : queues.entrySet()) {
324       String walGroupId = entry.getKey();
325       PriorityBlockingQueue<Path> queue = entry.getValue();
326       final ReplicationSourceWorkerThread worker =
327           new ReplicationSourceWorkerThread(walGroupId, queue, replicationQueueInfo, this);
328       ReplicationSourceWorkerThread extant = workerThreads.putIfAbsent(walGroupId, worker);
329       if (extant != null) {
330         LOG.debug("Someone has beat us to start a worker thread for wal group " + walGroupId);
331       } else {
332         LOG.debug("Starting up worker for wal group " + walGroupId);
333         worker.startup();
334       }
335     }
336   }
337 
338   /**
339    * Do the sleeping logic
340    * @param msg Why we sleep
341    * @param sleepMultiplier by how many times the default sleeping time is augmented
342    * @return True if <code>sleepMultiplier</code> is &lt; <code>maxRetriesMultiplier</code>
343    */
344   protected boolean sleepForRetries(String msg, int sleepMultiplier) {
345     try {
346       if (LOG.isTraceEnabled()) {
347         LOG.trace(msg + ", sleeping " + sleepForRetries + " times " + sleepMultiplier);
348       }
349       Thread.sleep(this.sleepForRetries * sleepMultiplier);
350     } catch (InterruptedException e) {
351       LOG.debug("Interrupted while sleeping between retries");
352       Thread.currentThread().interrupt();
353     }
354     return sleepMultiplier < maxRetriesMultiplier;
355   }
356 
357   /**
358    * check whether the peer is enabled or not
359    *
360    * @return true if the peer is enabled, otherwise false
361    */
362   protected boolean isPeerEnabled() {
363     return this.replicationPeers.getStatusOfPeer(this.peerId);
364   }
365 
366   @Override
367   public void startup() {
368     String n = Thread.currentThread().getName();
369     Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() {
370       @Override
371       public void uncaughtException(final Thread t, final Throwable e) {
372         LOG.error("Unexpected exception in ReplicationSource", e);
373       }
374     };
375     Threads
376         .setDaemonThreadRunning(this, n + ".replicationSource," + this.peerClusterZnode, handler);
377   }
378 
379   @Override
380   public void terminate(String reason) {
381     terminate(reason, null);
382   }
383 
384   @Override
385   public void terminate(String reason, Exception cause) {
386     terminate(reason, cause, true);
387   }
388 
389   public void terminate(String reason, Exception cause, boolean join) {
390     if (cause == null) {
391       LOG.info("Closing source "
392           + this.peerClusterZnode + " because: " + reason);
393 
394     } else {
395       LOG.error("Closing source " + this.peerClusterZnode
396           + " because an error occurred: " + reason, cause);
397     }
398     this.sourceRunning = false;
399     Collection<ReplicationSourceWorkerThread> workers = workerThreads.values();
400     for (ReplicationSourceWorkerThread worker : workers) {
401       worker.setWorkerState(WorkerState.STOPPED);
402       worker.interrupt();
403     }
404     ListenableFuture<Service.State> future = null;
405     if (this.replicationEndpoint != null) {
406       future = this.replicationEndpoint.stop();
407     }
408     if (join) {
409       for (ReplicationSourceWorkerThread worker : workers) {
410         Threads.shutdown(worker, this.sleepForRetries);
411         LOG.info("ReplicationSourceWorker " + worker.getName() + " terminated");
412       }
413       if (future != null) {
414         try {
415           future.get(sleepForRetries * maxRetriesMultiplier, TimeUnit.MILLISECONDS);
416         } catch (Exception e) {
417           LOG.warn("Got exception while waiting for endpoint to shutdown for replication source :"
418               + this.peerClusterZnode,
419             e);
420         }
421       }
422     }
423   }
424 
425   @Override
426   public String getPeerClusterZnode() {
427     return this.peerClusterZnode;
428   }
429 
430   @Override
431   public String getPeerClusterId() {
432     return this.peerId;
433   }
434 
435   @Override
436   public Path getCurrentPath() {
437     // only for testing
438     for (ReplicationSourceWorkerThread worker : workerThreads.values()) {
439       if (worker.getCurrentPath() != null) return worker.getCurrentPath();
440     }
441     return null;
442   }
443 
444   private boolean isSourceActive() {
445     return !this.stopper.isStopped() && this.sourceRunning;
446   }
447 
448   /**
449    * Comparator used to compare logs together based on their start time
450    */
451   public static class LogsComparator implements Comparator<Path> {
452 
453     @Override
454     public int compare(Path o1, Path o2) {
455       return Long.compare(getTS(o1), getTS(o2));
456     }
457 
458     /**
459      * Split a path to get the start time
460      * For example: 10.20.20.171%3A60020.1277499063250
461      * @param p path to split
462      * @return start time
463      */
464     private static long getTS(Path p) {
465       int tsIndex = p.getName().lastIndexOf('.') + 1;
466       return Long.parseLong(p.getName().substring(tsIndex));
467     }
468   }
469 
470   @Override
471   public String getStats() {
472     StringBuilder sb = new StringBuilder();
473     sb.append("Total replicated edits: ").append(totalReplicatedEdits)
474         .append(", current progress: \n");
475     for (Map.Entry<String, ReplicationSourceWorkerThread> entry : workerThreads.entrySet()) {
476       String walGroupId = entry.getKey();
477       ReplicationSourceWorkerThread worker = entry.getValue();
478       long position = worker.getCurrentPosition();
479       Path currentPath = worker.getCurrentPath();
480       sb.append("walGroup [").append(walGroupId).append("]: ");
481       if (currentPath != null) {
482         sb.append("currently replicating from: ").append(currentPath).append(" at position: ")
483             .append(position).append("\n");
484       } else {
485         sb.append("no replication ongoing, waiting for new log");
486       }
487     }
488     return sb.toString();
489   }
490 
491   @Override
492   public MetricsSource getSourceMetrics() {
493     return this.metrics;
494   }
495 
496   public class ReplicationSourceWorkerThread extends Thread {
497     private ReplicationSource source;
498     private String walGroupId;
499     private PriorityBlockingQueue<Path> queue;
500     private ReplicationQueueInfo replicationQueueInfo;
501     // Our reader for the current log. open/close handled by repLogReader
502     private WAL.Reader reader;
503     // Last position in the log that we sent to ZooKeeper
504     private long lastLoggedPosition = -1;
505     // Path of the current log
506     private volatile Path currentPath;
507     // Handle on the log reader helper
508     private ReplicationWALReaderManager repLogReader;
509     // Current number of operations (Put/Delete) that we need to replicate
510     private int currentNbOperations = 0;
511     // Current size of data we need to replicate
512     private int currentSize = 0;
513     // Current state of the worker thread
514     private WorkerState state;
515     // Current number of hfiles that we need to replicate
516     private long currentNbHFiles = 0;
517 
518 
519     public ReplicationSourceWorkerThread(String walGroupId, PriorityBlockingQueue<Path> queue,
520         ReplicationQueueInfo replicationQueueInfo, ReplicationSource source) {
521       this.walGroupId = walGroupId;
522       this.queue = queue;
523       this.replicationQueueInfo = replicationQueueInfo;
524       this.repLogReader = new ReplicationWALReaderManager(fs, conf);
525       this.source = source;
526     }
527 
528     @Override
529     public void run() {
530       setWorkerState(WorkerState.RUNNING);
531       // If this is recovered, the queue is already full and the first log
532       // normally has a position (unless the RS failed between 2 logs)
533       if (this.replicationQueueInfo.isQueueRecovered()) {
534         try {
535           this.repLogReader.setPosition(replicationQueues.getLogPosition(peerClusterZnode,
536             this.queue.peek().getName()));
537           if (LOG.isTraceEnabled()) {
538             LOG.trace("Recovered queue started with log " + this.queue.peek() + " at position "
539                 + this.repLogReader.getPosition());
540           }
541         } catch (ReplicationException e) {
542           terminate("Couldn't get the position of this recovered queue " + peerClusterZnode, e);
543         }
544       }
545       int sleepMultiplier = 1;
546       // Loop until we close down
547       while (isWorkerActive()) {
548         // Sleep until replication is enabled again
549         if (!isPeerEnabled()) {
550           if (sleepForRetries("Replication is disabled", sleepMultiplier)) {
551             sleepMultiplier++;
552           }
553           continue;
554         }
555         Path oldPath = getCurrentPath(); //note that in the current scenario,
556                                          //oldPath will be null when a log roll
557                                          //happens.
558         // Get a new path
559         boolean hasCurrentPath = getNextPath();
560         if (getCurrentPath() != null && oldPath == null) {
561           sleepMultiplier = 1; //reset the sleepMultiplier on a path change
562         }
563         if (!hasCurrentPath) {
564           if (sleepForRetries("No log to process", sleepMultiplier)) {
565             sleepMultiplier++;
566           }
567           continue;
568         }
569         boolean currentWALisBeingWrittenTo = false;
570         //For WAL files we own (rather than recovered), take a snapshot of whether the
571         //current WAL file (this.currentPath) is in use (for writing) NOW!
572         //Since the new WAL paths are enqueued only after the prev WAL file
573         //is 'closed', presence of an element in the queue means that
574         //the previous WAL file was closed, else the file is in use (currentPath)
575         //We take the snapshot now so that we are protected against races
576         //where a new file gets enqueued while the current file is being processed
577         //(and where we just finished reading the current file).
578         if (!this.replicationQueueInfo.isQueueRecovered() && queue.size() == 0) {
579           currentWALisBeingWrittenTo = true;
580         }
581         // Open a reader on it
582         if (!openReader(sleepMultiplier)) {
583           // Reset the sleep multiplier, else it'd be reused for the next file
584           sleepMultiplier = 1;
585           continue;
586         }
587 
588         // If we got a null reader but didn't continue, then sleep and continue
589         if (this.reader == null) {
590           if (sleepForRetries("Unable to open a reader", sleepMultiplier)) {
591             sleepMultiplier++;
592           }
593           continue;
594         }
595 
596         boolean gotIOE = false;
597         currentNbOperations = 0;
598         currentNbHFiles = 0;
599         List<WAL.Entry> entries = new ArrayList<WAL.Entry>(1);
600         currentSize = 0;
601         try {
602           if (readAllEntriesToReplicateOrNextFile(currentWALisBeingWrittenTo, entries)) {
603             continue;
604           }
605         } catch (IOException ioe) {
606           LOG.warn(peerClusterZnode + " Got: ", ioe);
607           gotIOE = true;
608           if (ioe.getCause() instanceof EOFException) {
609 
610             boolean considerDumping = false;
611             if (this.replicationQueueInfo.isQueueRecovered()) {
612               try {
613                 FileStatus stat = fs.getFileStatus(this.currentPath);
614                 if (stat.getLen() == 0) {
615                   LOG.warn(peerClusterZnode + " Got EOF and the file was empty");
616                 }
617                 considerDumping = true;
618               } catch (IOException e) {
619                 LOG.warn(peerClusterZnode + " Got while getting file size: ", e);
620               }
621             }
622 
623             if (considerDumping &&
624                 sleepMultiplier == maxRetriesMultiplier &&
625                 processEndOfFile(false)) {
626               continue;
627             }
628           }
629         } finally {
630           try {
631             this.reader = null;
632             this.repLogReader.closeReader();
633           } catch (IOException e) {
634             gotIOE = true;
635             LOG.warn("Unable to finalize the tailing of a file", e);
636           }
637         }
638 
639         // If we didn't get anything to replicate, or if we hit a IOE,
640         // wait a bit and retry.
641         // But if we need to stop, don't bother sleeping
642         if (isWorkerActive() && (gotIOE || entries.isEmpty())) {
643           if (this.lastLoggedPosition != this.repLogReader.getPosition()) {
644             manager.logPositionAndCleanOldLogs(this.currentPath,
645                 peerClusterZnode, this.repLogReader.getPosition(),
646                 this.replicationQueueInfo.isQueueRecovered(), currentWALisBeingWrittenTo);
647             this.lastLoggedPosition = this.repLogReader.getPosition();
648           }
649           // Reset the sleep multiplier if nothing has actually gone wrong
650           if (!gotIOE) {
651             sleepMultiplier = 1;
652             // if there was nothing to ship and it's not an error
653             // set "ageOfLastShippedOp" to <now> to indicate that we're current
654             metrics.setAgeOfLastShippedOp(EnvironmentEdgeManager.currentTime(), walGroupId);
655           }
656           if (sleepForRetries("Nothing to replicate", sleepMultiplier)) {
657             sleepMultiplier++;
658           }
659           continue;
660         }
661         sleepMultiplier = 1;
662         shipEdits(currentWALisBeingWrittenTo, entries);
663       }
664       if (replicationQueueInfo.isQueueRecovered() && getWorkerState() == WorkerState.FINISHED) {
665         // use synchronize to make sure one last thread will clean the queue
666         synchronized (workerThreads) {
667           Threads.sleep(100);// wait a short while for other worker thread to fully exit
668           boolean allOtherTaskDone = true;
669           for (ReplicationSourceWorkerThread worker : workerThreads.values()) {
670             if (!worker.equals(this) && worker.getWorkerState() != WorkerState.FINISHED) {
671               allOtherTaskDone = false;
672               break;
673             }
674           }
675           if (allOtherTaskDone) {
676             manager.closeRecoveredQueue(this.source);
677             LOG.info("Finished recovering queue " + peerClusterZnode
678                 + " with the following stats: " + getStats());
679           }
680         }
681       }
682       // If the worker exits run loop without finishing it's task, mark it as stopped.
683       if (state != WorkerState.FINISHED) {
684         setWorkerState(WorkerState.STOPPED);
685       }
686     }
687 
688     /**
689      * Read all the entries from the current log files and retain those that need to be replicated.
690      * Else, process the end of the current file.
691      * @param currentWALisBeingWrittenTo is the current WAL being written to
692      * @param entries resulting entries to be replicated
693      * @return true if we got nothing and went to the next file, false if we got entries
694      * @throws IOException
695      */
696     protected boolean readAllEntriesToReplicateOrNextFile(boolean currentWALisBeingWrittenTo,
697         List<WAL.Entry> entries) throws IOException {
698       long seenEntries = 0;
699       if (LOG.isTraceEnabled()) {
700         LOG.trace("Seeking in " + this.currentPath + " at position "
701             + this.repLogReader.getPosition());
702       }
703       this.repLogReader.seek();
704       long positionBeforeRead = this.repLogReader.getPosition();
705       WAL.Entry entry = this.repLogReader.readNextAndSetPosition();
706       while (entry != null) {
707         metrics.incrLogEditsRead();
708         seenEntries++;
709 
710         // don't replicate if the log entries have already been consumed by the cluster
711         if (replicationEndpoint.canReplicateToSameCluster()
712             || !entry.getKey().getClusterIds().contains(peerClusterId)) {
713           // Remove all KVs that should not be replicated
714           entry = walEntryFilter.filter(entry);
715           WALEdit edit = null;
716           WALKey logKey = null;
717           if (entry != null) {
718             edit = entry.getEdit();
719             logKey = entry.getKey();
720           }
721 
722           if (edit != null && edit.size() != 0) {
723             // Mark that the current cluster has the change
724             logKey.addClusterId(clusterId);
725             currentNbOperations += countDistinctRowKeys(edit);
726             entries.add(entry);
727             currentSize += entry.getEdit().heapSize();
728             currentSize += calculateTotalSizeOfStoreFiles(edit);
729           } else {
730             metrics.incrLogEditsFiltered();
731           }
732         }
733         // Stop if too many entries or too big
734         // FIXME check the relationship between single wal group and overall
735         if (currentSize >= replicationQueueSizeCapacity
736             || entries.size() >= replicationQueueNbCapacity) {
737           break;
738         }
739         try {
740           entry = this.repLogReader.readNextAndSetPosition();
741         } catch (IOException ie) {
742           LOG.debug("Break on IOE: " + ie.getMessage());
743           break;
744         }
745       }
746       metrics.incrLogReadInBytes(this.repLogReader.getPosition() - positionBeforeRead);
747       if (currentWALisBeingWrittenTo) {
748         return false;
749       }
750       // If we didn't get anything and the queue has an object, it means we
751       // hit the end of the file for sure
752       return seenEntries == 0 && processEndOfFile(false);
753     }
754 
755     /**
756      * Calculate the total size of all the store files
757      * @param edit edit to count row keys from
758      * @return the total size of the store files
759      */
760     private int calculateTotalSizeOfStoreFiles(WALEdit edit) {
761       List<Cell> cells = edit.getCells();
762       int totalStoreFilesSize = 0;
763 
764       int totalCells = edit.size();
765       for (int i = 0; i < totalCells; i++) {
766         if (CellUtil.matchingQualifier(cells.get(i), WALEdit.BULK_LOAD)) {
767           try {
768             BulkLoadDescriptor bld = WALEdit.getBulkLoadDescriptor(cells.get(i));
769             List<StoreDescriptor> stores = bld.getStoresList();
770             int totalStores = stores.size();
771             for (int j = 0; j < totalStores; j++) {
772               totalStoreFilesSize += stores.get(j).getStoreFileSizeBytes();
773             }
774           } catch (IOException e) {
775             LOG.error("Failed to deserialize bulk load entry from wal edit. "
776                 + "Size of HFiles part of cell will not be considered in replication "
777                 + "request size calculation.", e);
778           }
779         }
780       }
781       return totalStoreFilesSize;
782     }
783 
784     private void cleanUpHFileRefs(WALEdit edit) throws IOException {
785       String peerId = peerClusterZnode;
786       if (peerId.contains("-")) {
787         // peerClusterZnode will be in the form peerId + "-" + rsZNode.
788         // A peerId will not have "-" in its name, see HBASE-11394
789         peerId = peerClusterZnode.split("-")[0];
790       }
791       List<Cell> cells = edit.getCells();
792       int totalCells = cells.size();
793       for (int i = 0; i < totalCells; i++) {
794         Cell cell = cells.get(i);
795         if (CellUtil.matchingQualifier(cell, WALEdit.BULK_LOAD)) {
796           BulkLoadDescriptor bld = WALEdit.getBulkLoadDescriptor(cell);
797           List<StoreDescriptor> stores = bld.getStoresList();
798           int totalStores = stores.size();
799           for (int j = 0; j < totalStores; j++) {
800             List<String> storeFileList = stores.get(j).getStoreFileList();
801             manager.cleanUpHFileRefs(peerId, storeFileList);
802             metrics.decrSizeOfHFileRefsQueue(storeFileList.size());
803           }
804         }
805       }
806     }
807 
808     /**
809      * Poll for the next path
810      * @return true if a path was obtained, false if not
811      */
812     protected boolean getNextPath() {
813       try {
814         if (this.currentPath == null) {
815           this.currentPath = queue.poll(sleepForRetries, TimeUnit.MILLISECONDS);
816           metrics.decrSizeOfLogQueue();
817           if (this.currentPath != null) {
818             // For recovered queue: must use peerClusterZnode since peerId is a parsed value
819             manager.cleanOldLogs(this.currentPath.getName(), peerClusterZnode,
820               this.replicationQueueInfo.isQueueRecovered());
821             if (LOG.isTraceEnabled()) {
822               LOG.trace("New log: " + this.currentPath);
823             }
824           }
825         }
826       } catch (InterruptedException e) {
827         LOG.warn("Interrupted while reading edits", e);
828       }
829       return this.currentPath != null;
830     }
831 
832     /**
833      * Open a reader on the current path
834      *
835      * @param sleepMultiplier by how many times the default sleeping time is augmented
836      * @return true if we should continue with that file, false if we are over with it
837      */
838     protected boolean openReader(int sleepMultiplier) {
839       try {
840         try {
841           if (LOG.isTraceEnabled()) {
842             LOG.trace("Opening log " + this.currentPath);
843           }
844           this.reader = repLogReader.openReader(this.currentPath);
845         } catch (FileNotFoundException fnfe) {
846           if (this.replicationQueueInfo.isQueueRecovered()) {
847             // We didn't find the log in the archive directory, look if it still
848             // exists in the dead RS folder (there could be a chain of failures
849             // to look at)
850             List<String> deadRegionServers = this.replicationQueueInfo.getDeadRegionServers();
851             LOG.info("NB dead servers : " + deadRegionServers.size());
852             final Path walDir = FSUtils.getWALRootDir(conf);
853             for (String curDeadServerName : deadRegionServers) {
854               final Path deadRsDirectory = new Path(walDir,
855                   DefaultWALProvider.getWALDirectoryName(curDeadServerName));
856               Path[] locs = new Path[] {
857                   new Path(deadRsDirectory, currentPath.getName()),
858                   new Path(deadRsDirectory.suffix(DefaultWALProvider.SPLITTING_EXT),
859                                             currentPath.getName()),
860               };
861               for (Path possibleLogLocation : locs) {
862                 LOG.info("Possible location " + possibleLogLocation.toUri().toString());
863                 if (manager.getFs().exists(possibleLogLocation)) {
864                   // We found the right new location
865                   LOG.info("Log " + this.currentPath + " still exists at " +
866                       possibleLogLocation);
867                   // When running ReplicationSyncUp tool, we should replicate the data from WAL
868                   // which is moved to WAL splitting directory also.
869                   if (stopper instanceof ReplicationSyncUp.DummyServer) {
870                     // Open the log at the this location
871                     this.currentPath = possibleLogLocation;
872                     this.openReader(sleepMultiplier);
873                   }
874                   return true;
875                 }
876               }
877             }
878             // In the case of disaster/recovery, HMaster may be shutdown/crashed before flush data
879             // from .logs to .oldlogs. Loop into .logs folders and check whether a match exists
880             if (stopper instanceof ReplicationSyncUp.DummyServer) {
881               // N.B. the ReplicationSyncUp tool sets the manager.getWALDir to the root of the wal
882               //      area rather than to the wal area for a particular region server.
883               FileStatus[] rss = fs.listStatus(manager.getLogDir());
884               for (FileStatus rs : rss) {
885                 Path p = rs.getPath();
886                 FileStatus[] logs = fs.listStatus(p);
887                 for (FileStatus log : logs) {
888                   p = new Path(p, log.getPath().getName());
889                   if (p.getName().equals(currentPath.getName())) {
890                     currentPath = p;
891                     LOG.info("Log " + currentPath.getName() + " found at " + currentPath);
892                     // Open the log at the new location
893                     this.openReader(sleepMultiplier);
894                     return true;
895                   }
896                 }
897               }
898             }
899 
900             // TODO What happens if the log was missing from every single location?
901             // Although we need to check a couple of times as the log could have
902             // been moved by the master between the checks
903             // It can also happen if a recovered queue wasn't properly cleaned,
904             // such that the znode pointing to a log exists but the log was
905             // deleted a long time ago.
906             // For the moment, we'll throw the IO and processEndOfFile
907             throw new IOException("File from recovered queue is " +
908                 "nowhere to be found", fnfe);
909           } else {
910             // If the log was archived, continue reading from there
911             Path archivedLogLocation =
912                 new Path(manager.getOldLogDir(), currentPath.getName());
913             if (manager.getFs().exists(archivedLogLocation)) {
914               currentPath = archivedLogLocation;
915               LOG.info("Log " + this.currentPath + " was moved to " +
916                   archivedLogLocation);
917               // Open the log at the new location
918               this.openReader(sleepMultiplier);
919 
920             }
921             // TODO What happens the log is missing in both places?
922           }
923         }
924       } catch (LeaseNotRecoveredException lnre) {
925         // HBASE-15019 the WAL was not closed due to some hiccup.
926         LOG.warn(peerClusterZnode + " Try to recover the WAL lease " + currentPath, lnre);
927         recoverLease(conf, currentPath);
928         this.reader = null;
929       } catch (IOException ioe) {
930         if (ioe instanceof EOFException && isCurrentLogEmpty()) return true;
931         LOG.warn(peerClusterZnode + " Got: ", ioe);
932         this.reader = null;
933         if (ioe.getCause() instanceof NullPointerException) {
934           // Workaround for race condition in HDFS-4380
935           // which throws a NPE if we open a file before any data node has the most recent block
936           // Just sleep and retry. Will require re-reading compressed WALs for compressionContext.
937           LOG.warn("Got NPE opening reader, will retry.");
938         } else if (sleepMultiplier >= maxRetriesMultiplier
939             && conf.getBoolean("replication.source.eof.autorecovery", false)) {
940           // TODO Need a better way to determine if a file is really gone but
941           // TODO without scanning all logs dir
942           LOG.warn("Waited too long for this file, considering dumping");
943           return !processEndOfFile(true);
944         }
945       }
946       return true;
947     }
948 
949     private void recoverLease(final Configuration conf, final Path path) {
950       try {
951         final FileSystem dfs = FSUtils.getCurrentFileSystem(conf);
952         FSUtils fsUtils = FSUtils.getInstance(dfs, conf);
953         fsUtils.recoverFileLease(dfs, path, conf, new CancelableProgressable() {
954           @Override
955           public boolean progress() {
956             LOG.debug("recover WAL lease: " + path);
957             return isWorkerActive();
958           }
959         });
960       } catch (IOException e) {
961         LOG.warn("unable to recover lease for WAL: " + path, e);
962       }
963     }
964 
965     /*
966      * Checks whether the current log file is empty, and it is not a recovered queue. This is to
967      * handle scenario when in an idle cluster, there is no entry in the current log and we keep on
968      * trying to read the log file and get EOFException. In case of a recovered queue the last log
969      * file may be empty, and we don't want to retry that.
970      */
971     private boolean isCurrentLogEmpty() {
972       return (this.repLogReader.getPosition() == 0 &&
973           !this.replicationQueueInfo.isQueueRecovered() && queue.size() == 0);
974     }
975 
976     /**
977      * Count the number of different row keys in the given edit because of mini-batching. We assume
978      * that there's at least one Cell in the WALEdit.
979      * @param edit edit to count row keys from
980      * @return number of different row keys
981      */
982     private int countDistinctRowKeys(WALEdit edit) {
983       List<Cell> cells = edit.getCells();
984       int distinctRowKeys = 1;
985       int totalHFileEntries = 0;
986       Cell lastCell = cells.get(0);
987       
988       int totalCells = edit.size();
989       for (int i = 0; i < totalCells; i++) {
990         // Count HFiles to be replicated
991         if (CellUtil.matchingQualifier(cells.get(i), WALEdit.BULK_LOAD)) {
992           try {
993             BulkLoadDescriptor bld = WALEdit.getBulkLoadDescriptor(cells.get(i));
994             List<StoreDescriptor> stores = bld.getStoresList();
995             int totalStores = stores.size();
996             for (int j = 0; j < totalStores; j++) {
997               totalHFileEntries += stores.get(j).getStoreFileList().size();
998             }
999           } catch (IOException e) {
1000             LOG.error("Failed to deserialize bulk load entry from wal edit. "
1001                 + "Then its hfiles count will not be added into metric.");
1002           }
1003         }
1004 
1005         if (!CellUtil.matchingRow(cells.get(i), lastCell)) {
1006           distinctRowKeys++;
1007         }
1008         lastCell = cells.get(i);
1009       }
1010       currentNbHFiles += totalHFileEntries;
1011       return distinctRowKeys + totalHFileEntries;
1012     }
1013 
1014     /**
1015      * Do the shipping logic
1016      * @param currentWALisBeingWrittenTo was the current WAL being (seemingly)
1017      * written to when this method was called
1018      */
1019     protected void shipEdits(boolean currentWALisBeingWrittenTo, List<WAL.Entry> entries) {
1020       int sleepMultiplier = 0;
1021       if (entries.isEmpty()) {
1022         LOG.warn("Was given 0 edits to ship");
1023         return;
1024       }
1025       while (isWorkerActive()) {
1026         try {
1027           if (throttler.isEnabled()) {
1028             long sleepTicks = throttler.getNextSleepInterval(currentSize);
1029             if (sleepTicks > 0) {
1030               try {
1031                 if (LOG.isTraceEnabled()) {
1032                   LOG.trace("To sleep " + sleepTicks + "ms for throttling control");
1033                 }
1034                 Thread.sleep(sleepTicks);
1035               } catch (InterruptedException e) {
1036                 LOG.debug("Interrupted while sleeping for throttling control");
1037                 Thread.currentThread().interrupt();
1038                 // current thread might be interrupted to terminate
1039                 // directly go back to while() for confirm this
1040                 continue;
1041               }
1042               // reset throttler's cycle start tick when sleep for throttling occurs
1043               throttler.resetStartTick();
1044             }
1045           }
1046           // create replicateContext here, so the entries can be GC'd upon return from this call
1047           // stack
1048           ReplicationEndpoint.ReplicateContext replicateContext =
1049               new ReplicationEndpoint.ReplicateContext();
1050           replicateContext.setEntries(entries).setSize(currentSize);
1051           replicateContext.setWalGroupId(walGroupId);
1052 
1053           long startTimeNs = System.nanoTime();
1054           // send the edits to the endpoint. Will block until the edits are shipped and acknowledged
1055           boolean replicated = replicationEndpoint.replicate(replicateContext);
1056           long endTimeNs = System.nanoTime();
1057 
1058           if (!replicated) {
1059             continue;
1060           } else {
1061             sleepMultiplier = Math.max(sleepMultiplier - 1, 0);
1062           }
1063 
1064           if (this.lastLoggedPosition != this.repLogReader.getPosition()) {
1065             //Clean up hfile references
1066             int size = entries.size();
1067             for (int i = 0; i < size; i++) {
1068               cleanUpHFileRefs(entries.get(i).getEdit());
1069 
1070               TableName tableName = entries.get(i).getKey().getTablename();
1071               source.getSourceMetrics().setAgeOfLastShippedOpByTable(
1072                 entries.get(i).getKey().getWriteTime(),
1073                 tableName.getNameAsString());
1074             }
1075             //Log and clean up WAL logs
1076             manager.logPositionAndCleanOldLogs(this.currentPath, peerClusterZnode,
1077               this.repLogReader.getPosition(), this.replicationQueueInfo.isQueueRecovered(),
1078               currentWALisBeingWrittenTo);
1079             this.lastLoggedPosition = this.repLogReader.getPosition();
1080           }
1081           if (throttler.isEnabled()) {
1082             throttler.addPushSize(currentSize);
1083           }
1084           totalReplicatedEdits.addAndGet(entries.size());
1085           totalReplicatedOperations.addAndGet(currentNbOperations);
1086           // FIXME check relationship between wal group and overall
1087           metrics.shipBatch(currentNbOperations, currentSize, currentNbHFiles);
1088           metrics.setAgeOfLastShippedOp(entries.get(entries.size() - 1).getKey().getWriteTime(),
1089             walGroupId);
1090           if (LOG.isTraceEnabled()) {
1091             LOG.trace("Replicated " + totalReplicatedEdits + " entries in total, or "
1092                 + totalReplicatedOperations + " operations in "
1093                 + ((endTimeNs - startTimeNs) / 1000000) + " ms");
1094           }
1095           break;
1096         } catch (Exception ex) {
1097           LOG.warn(replicationEndpoint.getClass().getName() + " threw unknown exception:"
1098               + org.apache.hadoop.util.StringUtils.stringifyException(ex));
1099           if (sleepForRetries("ReplicationEndpoint threw exception", sleepMultiplier)) {
1100             sleepMultiplier++;
1101           }
1102         }
1103       }
1104     }
1105 
1106     /**
1107      * If the queue isn't empty, switch to the next one Else if this is a recovered queue, it means
1108      * we're done! Else we'll just continue to try reading the log file
1109      * @return true if we're done with the current file, false if we should continue trying to read
1110      *         from it
1111      */
1112     @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "DE_MIGHT_IGNORE",
1113         justification = "Yeah, this is how it works")
1114     protected boolean processEndOfFile(boolean dumpOnlyIfZeroLength) {
1115       // We presume this means the file we're reading is closed.
1116       if (this.queue.size() != 0) {
1117         // -1 means the wal wasn't closed cleanly.
1118         final long trailerSize = this.repLogReader.currentTrailerSize();
1119         final long currentPosition = this.repLogReader.getPosition();
1120         FileStatus stat = null;
1121         try {
1122           stat = fs.getFileStatus(this.currentPath);
1123         } catch (IOException exception) {
1124           LOG.warn("Couldn't get file length information about log " + this.currentPath + ", it " + (trailerSize < 0 ? "was not" : "was") + " closed cleanly"
1125               + ", stats: " + getStats());
1126           metrics.incrUnknownFileLengthForClosedWAL();
1127         }
1128         if (stat != null) {
1129           if (trailerSize < 0) {
1130             if (currentPosition < stat.getLen()) {
1131               final long skippedBytes = stat.getLen() - currentPosition;
1132               LOG.info("Reached the end of WAL file '" + currentPath + "'. It was not closed cleanly, so we did not parse " + skippedBytes + " bytes of data.");
1133               metrics.incrUncleanlyClosedWALs();
1134               metrics.incrBytesSkippedInUncleanlyClosedWALs(skippedBytes);
1135             }
1136           } else if (currentPosition + trailerSize < stat.getLen()){
1137             LOG.warn("Processing end of WAL file '" + currentPath + "'. At position " + currentPosition + ", which is too far away from reported file length " + stat.getLen() +
1138                 ". Restarting WAL reading (see HBASE-15983 for details). stats: " + getStats());
1139             repLogReader.setPosition(0);
1140             metrics.incrRestartedWALReading();
1141             metrics.incrRepeatedFileBytes(currentPosition);
1142             return false;
1143           }
1144         }
1145         if (LOG.isTraceEnabled()) {
1146           LOG.trace("Reached the end of log " + this.currentPath + ", stats: " + getStats()
1147               + ", and the length of the file is " + (stat == null ? "N/A" : stat.getLen()));
1148         }
1149         if (dumpOnlyIfZeroLength && stat.getLen() != 0) {
1150           return false;
1151         }
1152         this.currentPath = null;
1153         this.repLogReader.finishCurrentFile();
1154         this.reader = null;
1155         metrics.incrCompletedWAL();
1156         return true;
1157       } else if (this.replicationQueueInfo.isQueueRecovered()) {
1158         LOG.debug("Finished recovering queue for group " + walGroupId + " of peer "
1159             + peerClusterZnode);
1160         metrics.incrCompletedRecoveryQueue();
1161         setWorkerState(WorkerState.FINISHED);
1162         return true;
1163       }
1164       return false;
1165     }
1166 
1167     public void startup() {
1168       String n = Thread.currentThread().getName();
1169       Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() {
1170         @Override
1171         public void uncaughtException(final Thread t, final Throwable e) {
1172           RSRpcServices.exitIfOOME(e);
1173           LOG.error("Unexpected exception in ReplicationSourceWorkerThread," + " currentPath="
1174               + getCurrentPath(), e);
1175           stopper.stop("Unexpected exception in ReplicationSourceWorkerThread");
1176         }
1177       };
1178       Threads.setDaemonThreadRunning(this, n + ".replicationSource." + walGroupId + ","
1179           + peerClusterZnode, handler);
1180       workerThreads.put(walGroupId, this);
1181     }
1182 
1183     public Path getCurrentPath() {
1184       return this.currentPath;
1185     }
1186 
1187     public long getCurrentPosition() {
1188       return this.repLogReader.getPosition();
1189     }
1190 
1191     private boolean isWorkerActive() {
1192       return !stopper.isStopped() && state == WorkerState.RUNNING && !isInterrupted();
1193     }
1194 
1195     private void terminate(String reason, Exception cause) {
1196       if (cause == null) {
1197         LOG.info("Closing worker for wal group " + this.walGroupId + " because: " + reason);
1198 
1199       } else {
1200         LOG.error("Closing worker for wal group " + this.walGroupId
1201             + " because an error occurred: " + reason, cause);
1202       }
1203       setWorkerState(WorkerState.STOPPED);
1204       this.interrupt();
1205       Threads.shutdown(this, sleepForRetries);
1206       LOG.info("ReplicationSourceWorker " + this.getName() + " terminated");
1207     }
1208 
1209     /**
1210      * Set the worker state
1211      * @param state
1212      */
1213     public void setWorkerState(WorkerState state) {
1214       this.state = state;
1215     }
1216 
1217     /**
1218      * Get the current state of this worker.
1219      * @return WorkerState
1220      */
1221     public WorkerState getWorkerState() {
1222       return state;
1223     }
1224   }
1225 }