001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.wal;
019
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Objects;
026import java.util.concurrent.atomic.AtomicBoolean;
027import java.util.regex.Pattern;
028
029import org.apache.hadoop.conf.Configuration;
030import org.apache.hadoop.fs.FSDataInputStream;
031import org.apache.hadoop.fs.FileSystem;
032import org.apache.hadoop.fs.Path;
033import org.apache.hadoop.hbase.HConstants;
034import org.apache.hadoop.hbase.ServerName;
035import org.apache.hadoop.hbase.client.RegionInfo;
036import org.apache.yetus.audience.InterfaceAudience;
037import org.apache.yetus.audience.InterfaceStability;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040import org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL;
041import org.apache.hadoop.hbase.regionserver.wal.WALActionsListener;
042import org.apache.hadoop.hbase.util.CancelableProgressable;
043import org.apache.hadoop.hbase.util.FSUtils;
044import org.apache.hadoop.hbase.util.LeaseNotRecoveredException;
045import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
046
047/**
048 * Base class of a WAL Provider that returns a single thread safe WAL that writes to Hadoop FS. By
049 * default, this implementation picks a directory in Hadoop FS based on a combination of
050 * <ul>
051 * <li>the HBase root directory
052 * <li>HConstants.HREGION_LOGDIR_NAME
053 * <li>the given factory's factoryId (usually identifying the regionserver by host:port)
054 * </ul>
055 * It also uses the providerId to differentiate among files.
056 */
057@InterfaceAudience.Private
058@InterfaceStability.Evolving
059public abstract class AbstractFSWALProvider<T extends AbstractFSWAL<?>> implements WALProvider {
060
061  private static final Logger LOG = LoggerFactory.getLogger(AbstractFSWALProvider.class);
062
063  /** Separate old log into different dir by regionserver name **/
064  public static final String SEPARATE_OLDLOGDIR = "hbase.separate.oldlogdir.by.regionserver";
065  public static final boolean DEFAULT_SEPARATE_OLDLOGDIR = false;
066
067  // Only public so classes back in regionserver.wal can access
068  public interface Reader extends WAL.Reader {
069    /**
070     * @param fs File system.
071     * @param path Path.
072     * @param c Configuration.
073     * @param s Input stream that may have been pre-opened by the caller; may be null.
074     */
075    void init(FileSystem fs, Path path, Configuration c, FSDataInputStream s) throws IOException;
076  }
077
078  protected volatile T wal;
079  protected WALFactory factory;
080  protected Configuration conf;
081  protected List<WALActionsListener> listeners = new ArrayList<>();
082  protected String providerId;
083  protected AtomicBoolean initialized = new AtomicBoolean(false);
084  // for default wal provider, logPrefix won't change
085  protected String logPrefix;
086
087  /**
088   * we synchronized on walCreateLock to prevent wal recreation in different threads
089   */
090  private final Object walCreateLock = new Object();
091
092  /**
093   * @param factory factory that made us, identity used for FS layout. may not be null
094   * @param conf may not be null
095   * @param providerId differentiate between providers from one factory, used for FS layout. may be
096   *          null
097   */
098  @Override
099  public void init(WALFactory factory, Configuration conf, String providerId) throws IOException {
100    if (!initialized.compareAndSet(false, true)) {
101      throw new IllegalStateException("WALProvider.init should only be called once.");
102    }
103    this.factory = factory;
104    this.conf = conf;
105    this.providerId = providerId;
106    // get log prefix
107    StringBuilder sb = new StringBuilder().append(factory.factoryId);
108    if (providerId != null) {
109      if (providerId.startsWith(WAL_FILE_NAME_DELIMITER)) {
110        sb.append(providerId);
111      } else {
112        sb.append(WAL_FILE_NAME_DELIMITER).append(providerId);
113      }
114    }
115    logPrefix = sb.toString();
116    doInit(conf);
117  }
118
119  @Override
120  public List<WAL> getWALs() {
121    if (wal == null) {
122      return Collections.emptyList();
123    }
124    List<WAL> wals = new ArrayList<>(1);
125    wals.add(wal);
126    return wals;
127  }
128
129  @Override
130  public T getWAL(RegionInfo region) throws IOException {
131    T walCopy = wal;
132    if (walCopy == null) {
133      // only lock when need to create wal, and need to lock since
134      // creating hlog on fs is time consuming
135      synchronized (walCreateLock) {
136        walCopy = wal;
137        if (walCopy == null) {
138          walCopy = createWAL();
139          wal = walCopy;
140        }
141      }
142    }
143    return walCopy;
144  }
145
146  protected abstract T createWAL() throws IOException;
147
148  protected abstract void doInit(Configuration conf) throws IOException;
149
150  @Override
151  public void shutdown() throws IOException {
152    T log = this.wal;
153    if (log != null) {
154      log.shutdown();
155    }
156  }
157
158  @Override
159  public void close() throws IOException {
160    T log = this.wal;
161    if (log != null) {
162      log.close();
163    }
164  }
165
166  /**
167   * iff the given WALFactory is using the DefaultWALProvider for meta and/or non-meta, count the
168   * number of files (rolled and active). if either of them aren't, count 0 for that provider.
169   */
170  @Override
171  public long getNumLogFiles() {
172    T log = this.wal;
173    return log == null ? 0 : log.getNumLogFiles();
174  }
175
176  /**
177   * iff the given WALFactory is using the DefaultWALProvider for meta and/or non-meta, count the
178   * size of files (only rolled). if either of them aren't, count 0 for that provider.
179   */
180  @Override
181  public long getLogFileSize() {
182    T log = this.wal;
183    return log == null ? 0 : log.getLogFileSize();
184  }
185
186  /**
187   * returns the number of rolled WAL files.
188   */
189  @VisibleForTesting
190  public static int getNumRolledLogFiles(WAL wal) {
191    return ((AbstractFSWAL<?>) wal).getNumRolledLogFiles();
192  }
193
194  /**
195   * returns the size of rolled WAL files.
196   */
197  @VisibleForTesting
198  public static long getLogFileSize(WAL wal) {
199    return ((AbstractFSWAL<?>) wal).getLogFileSize();
200  }
201
202  /**
203   * return the current filename from the current wal.
204   */
205  @VisibleForTesting
206  public static Path getCurrentFileName(final WAL wal) {
207    return ((AbstractFSWAL<?>) wal).getCurrentFileName();
208  }
209
210  /**
211   * request a log roll, but don't actually do it.
212   */
213  @VisibleForTesting
214  static void requestLogRoll(final WAL wal) {
215    ((AbstractFSWAL<?>) wal).requestLogRoll();
216  }
217
218  // should be package private; more visible for use in AbstractFSWAL
219  public static final String WAL_FILE_NAME_DELIMITER = ".";
220  /** The hbase:meta region's WAL filename extension */
221  @VisibleForTesting
222  public static final String META_WAL_PROVIDER_ID = ".meta";
223  static final String DEFAULT_PROVIDER_ID = "default";
224
225  // Implementation details that currently leak in tests or elsewhere follow
226  /** File Extension used while splitting an WAL into regions (HBASE-2312) */
227  public static final String SPLITTING_EXT = "-splitting";
228
229  /**
230   * It returns the file create timestamp from the file name. For name format see
231   * {@link #validateWALFilename(String)} public until remaining tests move to o.a.h.h.wal
232   * @param wal must not be null
233   * @return the file number that is part of the WAL file name
234   */
235  @VisibleForTesting
236  public static long extractFileNumFromWAL(final WAL wal) {
237    final Path walName = ((AbstractFSWAL<?>) wal).getCurrentFileName();
238    if (walName == null) {
239      throw new IllegalArgumentException("The WAL path couldn't be null");
240    }
241    final String[] walPathStrs = walName.toString().split("\\" + WAL_FILE_NAME_DELIMITER);
242    return Long.parseLong(walPathStrs[walPathStrs.length - (isMetaFile(walName) ? 2 : 1)]);
243  }
244
245  /**
246   * Pattern used to validate a WAL file name see {@link #validateWALFilename(String)} for
247   * description.
248   */
249  private static final Pattern pattern =
250    Pattern.compile(".*\\.\\d*(" + META_WAL_PROVIDER_ID + ")*");
251
252  /**
253   * A WAL file name is of the format: &lt;wal-name&gt;{@link #WAL_FILE_NAME_DELIMITER}
254   * &lt;file-creation-timestamp&gt;[.meta]. provider-name is usually made up of a server-name and a
255   * provider-id
256   * @param filename name of the file to validate
257   * @return <tt>true</tt> if the filename matches an WAL, <tt>false</tt> otherwise
258   */
259  public static boolean validateWALFilename(String filename) {
260    return pattern.matcher(filename).matches();
261  }
262
263  /**
264   * Construct the directory name for all WALs on a given server. Dir names currently look like this
265   * for WALs: <code>hbase//WALs/kalashnikov.att.net,61634,1486865297088</code>.
266   * @param serverName Server name formatted as described in {@link ServerName}
267   * @return the relative WAL directory name, e.g. <code>.logs/1.example.org,60030,12345</code> if
268   *         <code>serverName</code> passed is <code>1.example.org,60030,12345</code>
269   */
270  public static String getWALDirectoryName(final String serverName) {
271    StringBuilder dirName = new StringBuilder(HConstants.HREGION_LOGDIR_NAME);
272    dirName.append("/");
273    dirName.append(serverName);
274    return dirName.toString();
275  }
276
277  /**
278   * Construct the directory name for all old WALs on a given server. The default old WALs dir looks
279   * like: <code>hbase/oldWALs</code>. If you config hbase.separate.oldlogdir.by.regionserver to
280   * true, it looks like <code>hbase//oldWALs/kalashnikov.att.net,61634,1486865297088</code>.
281   * @param conf
282   * @param serverName Server name formatted as described in {@link ServerName}
283   * @return the relative WAL directory name
284   */
285  public static String getWALArchiveDirectoryName(Configuration conf, final String serverName) {
286    StringBuilder dirName = new StringBuilder(HConstants.HREGION_OLDLOGDIR_NAME);
287    if (conf.getBoolean(SEPARATE_OLDLOGDIR, DEFAULT_SEPARATE_OLDLOGDIR)) {
288      dirName.append(Path.SEPARATOR);
289      dirName.append(serverName);
290    }
291    return dirName.toString();
292  }
293
294  /**
295   * Pulls a ServerName out of a Path generated according to our layout rules. In the below layouts,
296   * this method ignores the format of the logfile component. Current format: [base directory for
297   * hbase]/hbase/.logs/ServerName/logfile or [base directory for
298   * hbase]/hbase/.logs/ServerName-splitting/logfile Expected to work for individual log files and
299   * server-specific directories.
300   * @return null if it's not a log file. Returns the ServerName of the region server that created
301   *         this log file otherwise.
302   */
303  public static ServerName getServerNameFromWALDirectoryName(Configuration conf, String path)
304      throws IOException {
305    if (path == null || path.length() <= HConstants.HREGION_LOGDIR_NAME.length()) {
306      return null;
307    }
308
309    if (conf == null) {
310      throw new IllegalArgumentException("parameter conf must be set");
311    }
312
313    final String rootDir = conf.get(HConstants.HBASE_DIR);
314    if (rootDir == null || rootDir.isEmpty()) {
315      throw new IllegalArgumentException(HConstants.HBASE_DIR + " key not found in conf.");
316    }
317
318    final StringBuilder startPathSB = new StringBuilder(rootDir);
319    if (!rootDir.endsWith("/")) {
320      startPathSB.append('/');
321    }
322    startPathSB.append(HConstants.HREGION_LOGDIR_NAME);
323    if (!HConstants.HREGION_LOGDIR_NAME.endsWith("/")) {
324      startPathSB.append('/');
325    }
326    final String startPath = startPathSB.toString();
327
328    String fullPath;
329    try {
330      fullPath = FileSystem.get(conf).makeQualified(new Path(path)).toString();
331    } catch (IllegalArgumentException e) {
332      LOG.info("Call to makeQualified failed on " + path + " " + e.getMessage());
333      return null;
334    }
335
336    if (!fullPath.startsWith(startPath)) {
337      return null;
338    }
339
340    final String serverNameAndFile = fullPath.substring(startPath.length());
341
342    if (serverNameAndFile.indexOf('/') < "a,0,0".length()) {
343      // Either it's a file (not a directory) or it's not a ServerName format
344      return null;
345    }
346
347    Path p = new Path(path);
348    return getServerNameFromWALDirectoryName(p);
349  }
350
351  /**
352   * This function returns region server name from a log file name which is in one of the following
353   * formats:
354   * <ul>
355   * <li>hdfs://&lt;name node&gt;/hbase/.logs/&lt;server name&gt;-splitting/...</li>
356   * <li>hdfs://&lt;name node&gt;/hbase/.logs/&lt;server name&gt;/...</li>
357   * </ul>
358   * @return null if the passed in logFile isn't a valid WAL file path
359   */
360  public static ServerName getServerNameFromWALDirectoryName(Path logFile) {
361    String logDirName = logFile.getParent().getName();
362    // We were passed the directory and not a file in it.
363    if (logDirName.equals(HConstants.HREGION_LOGDIR_NAME)) {
364      logDirName = logFile.getName();
365    }
366    ServerName serverName = null;
367    if (logDirName.endsWith(SPLITTING_EXT)) {
368      logDirName = logDirName.substring(0, logDirName.length() - SPLITTING_EXT.length());
369    }
370    try {
371      serverName = ServerName.parseServerName(logDirName);
372    } catch (IllegalArgumentException | IllegalStateException ex) {
373      serverName = null;
374      LOG.warn("Cannot parse a server name from path=" + logFile + "; " + ex.getMessage());
375    }
376    if (serverName != null && serverName.getStartcode() < 0) {
377      LOG.warn("Invalid log file path=" + logFile);
378      serverName = null;
379    }
380    return serverName;
381  }
382
383  public static boolean isMetaFile(Path p) {
384    return isMetaFile(p.getName());
385  }
386
387  public static boolean isMetaFile(String p) {
388    if (p != null && p.endsWith(META_WAL_PROVIDER_ID)) {
389      return true;
390    }
391    return false;
392  }
393
394  public static boolean isArchivedLogFile(Path p) {
395    String oldLog = Path.SEPARATOR + HConstants.HREGION_OLDLOGDIR_NAME + Path.SEPARATOR;
396    return p.toString().contains(oldLog);
397  }
398
399  /**
400   * Get the archived WAL file path
401   * @param path - active WAL file path
402   * @param conf - configuration
403   * @return archived path if exists, path - otherwise
404   * @throws IOException exception
405   */
406  public static Path getArchivedLogPath(Path path, Configuration conf) throws IOException {
407    Path rootDir = FSUtils.getRootDir(conf);
408    Path oldLogDir = new Path(rootDir, HConstants.HREGION_OLDLOGDIR_NAME);
409    if (conf.getBoolean(SEPARATE_OLDLOGDIR, DEFAULT_SEPARATE_OLDLOGDIR)) {
410      ServerName serverName = getServerNameFromWALDirectoryName(path);
411      if (serverName == null) {
412        LOG.error("Couldn't locate log: " + path);
413        return path;
414      }
415      oldLogDir = new Path(oldLogDir, serverName.getServerName());
416    }
417    Path archivedLogLocation = new Path(oldLogDir, path.getName());
418    final FileSystem fs = FSUtils.getCurrentFileSystem(conf);
419
420    if (fs.exists(archivedLogLocation)) {
421      LOG.info("Log " + path + " was moved to " + archivedLogLocation);
422      return archivedLogLocation;
423    } else {
424      LOG.error("Couldn't locate log: " + path);
425      return path;
426    }
427  }
428
429  /**
430   * Opens WAL reader with retries and additional exception handling
431   * @param path path to WAL file
432   * @param conf configuration
433   * @return WAL Reader instance
434   * @throws IOException
435   */
436  public static org.apache.hadoop.hbase.wal.WAL.Reader openReader(Path path, Configuration conf)
437      throws IOException
438
439  {
440    long retryInterval = 2000; // 2 sec
441    int maxAttempts = 30;
442    int attempt = 0;
443    Exception ee = null;
444    org.apache.hadoop.hbase.wal.WAL.Reader reader = null;
445    while (reader == null && attempt++ < maxAttempts) {
446      try {
447        // Detect if this is a new file, if so get a new reader else
448        // reset the current reader so that we see the new data
449        reader = WALFactory.createReader(path.getFileSystem(conf), path, conf);
450        return reader;
451      } catch (FileNotFoundException fnfe) {
452        // If the log was archived, continue reading from there
453        Path archivedLog = AbstractFSWALProvider.getArchivedLogPath(path, conf);
454        if (!Objects.equals(path, archivedLog)) {
455          return openReader(archivedLog, conf);
456        } else {
457          throw fnfe;
458        }
459      } catch (LeaseNotRecoveredException lnre) {
460        // HBASE-15019 the WAL was not closed due to some hiccup.
461        LOG.warn("Try to recover the WAL lease " + path, lnre);
462        recoverLease(conf, path);
463        reader = null;
464        ee = lnre;
465      } catch (NullPointerException npe) {
466        // Workaround for race condition in HDFS-4380
467        // which throws a NPE if we open a file before any data node has the most recent block
468        // Just sleep and retry. Will require re-reading compressed WALs for compressionContext.
469        LOG.warn("Got NPE opening reader, will retry.");
470        reader = null;
471        ee = npe;
472      }
473      if (reader == null) {
474        // sleep before next attempt
475        try {
476          Thread.sleep(retryInterval);
477        } catch (InterruptedException e) {
478        }
479      }
480    }
481    throw new IOException("Could not open reader", ee);
482  }
483
484  // For HBASE-15019
485  private static void recoverLease(final Configuration conf, final Path path) {
486    try {
487      final FileSystem dfs = FSUtils.getCurrentFileSystem(conf);
488      FSUtils fsUtils = FSUtils.getInstance(dfs, conf);
489      fsUtils.recoverFileLease(dfs, path, conf, new CancelableProgressable() {
490        @Override
491        public boolean progress() {
492          LOG.debug("Still trying to recover WAL lease: " + path);
493          return true;
494        }
495      });
496    } catch (IOException e) {
497      LOG.warn("unable to recover lease for WAL: " + path, e);
498    }
499  }
500
501  @Override
502  public void addWALActionsListener(WALActionsListener listener) {
503    listeners.add(listener);
504  }
505
506  /**
507   * Get prefix of the log from its name, assuming WAL name in format of
508   * log_prefix.filenumber.log_suffix
509   * @param name Name of the WAL to parse
510   * @return prefix of the log
511   * @see AbstractFSWAL#getCurrentFileName()
512   */
513  public static String getWALPrefixFromWALName(String name) {
514    int endIndex = name.replaceAll(META_WAL_PROVIDER_ID, "").lastIndexOf(".");
515    return name.substring(0, endIndex);
516  }
517}