001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.hadoop.hbase.master;
020
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.util.Comparator;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.TreeMap;
027import java.util.concurrent.atomic.AtomicBoolean;
028import java.util.concurrent.atomic.AtomicInteger;
029
030import org.apache.hadoop.fs.FileSystem;
031import org.apache.hadoop.fs.Path;
032import org.apache.hadoop.hbase.HConstants;
033import org.apache.hadoop.hbase.MetaTableAccessor;
034import org.apache.hadoop.hbase.ScheduledChore;
035import org.apache.hadoop.hbase.TableName;
036import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
037import org.apache.hadoop.hbase.client.Connection;
038import org.apache.hadoop.hbase.client.RegionInfo;
039import org.apache.hadoop.hbase.client.Result;
040import org.apache.hadoop.hbase.client.TableDescriptor;
041import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
042import org.apache.hadoop.hbase.master.assignment.GCMergedRegionsProcedure;
043import org.apache.hadoop.hbase.master.assignment.GCRegionProcedure;
044import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
045import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
046import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
047import org.apache.hadoop.hbase.util.Bytes;
048import org.apache.hadoop.hbase.util.FSUtils;
049import org.apache.hadoop.hbase.util.Pair;
050import org.apache.hadoop.hbase.util.PairOfSameType;
051import org.apache.hadoop.hbase.util.Threads;
052import org.apache.hadoop.hbase.util.Triple;
053import org.apache.yetus.audience.InterfaceAudience;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057/**
058 * A janitor for the catalog tables.  Scans the <code>hbase:meta</code> catalog
059 * table on a period looking for unused regions to garbage collect.
060 */
061@InterfaceAudience.Private
062public class CatalogJanitor extends ScheduledChore {
063  private static final Logger LOG = LoggerFactory.getLogger(CatalogJanitor.class.getName());
064
065  private final AtomicBoolean alreadyRunning = new AtomicBoolean(false);
066  private final AtomicBoolean enabled = new AtomicBoolean(true);
067  private final MasterServices services;
068  private final Connection connection;
069  // PID of the last Procedure launched herein. Keep around for Tests.
070
071  CatalogJanitor(final MasterServices services) {
072    super("CatalogJanitor-" + services.getServerName().toShortString(), services,
073      services.getConfiguration().getInt("hbase.catalogjanitor.interval", 300000));
074    this.services = services;
075    this.connection = services.getConnection();
076  }
077
078  @Override
079  protected boolean initialChore() {
080    try {
081      if (this.enabled.get()) scan();
082    } catch (IOException e) {
083      LOG.warn("Failed initial scan of catalog table", e);
084      return false;
085    }
086    return true;
087  }
088
089  /**
090   * @param enabled
091   */
092  public boolean setEnabled(final boolean enabled) {
093    boolean alreadyEnabled = this.enabled.getAndSet(enabled);
094    // If disabling is requested on an already enabled chore, we could have an active
095    // scan still going on, callers might not be aware of that and do further action thinkng
096    // that no action would be from this chore.  In this case, the right action is to wait for
097    // the active scan to complete before exiting this function.
098    if (!enabled && alreadyEnabled) {
099      while (alreadyRunning.get()) {
100        Threads.sleepWithoutInterrupt(100);
101      }
102    }
103    return alreadyEnabled;
104  }
105
106  boolean getEnabled() {
107    return this.enabled.get();
108  }
109
110  @Override
111  protected void chore() {
112    try {
113      AssignmentManager am = this.services.getAssignmentManager();
114      if (this.enabled.get() && !this.services.isInMaintenanceMode() && am != null &&
115        am.isMetaLoaded() && !am.hasRegionsInTransition()) {
116        scan();
117      } else {
118        LOG.warn("CatalogJanitor is disabled! Enabled=" + this.enabled.get() +
119          ", maintenanceMode=" + this.services.isInMaintenanceMode() + ", am=" + am +
120          ", metaLoaded=" + (am != null && am.isMetaLoaded()) + ", hasRIT=" +
121          (am != null && am.hasRegionsInTransition()));
122      }
123    } catch (IOException e) {
124      LOG.warn("Failed scan of catalog table", e);
125    }
126  }
127
128  /**
129   * Scans hbase:meta and returns a number of scanned rows, and a map of merged
130   * regions, and an ordered map of split parents.
131   * @return triple of scanned rows, map of merged regions and map of split
132   *         parent regioninfos
133   * @throws IOException
134   */
135  Triple<Integer, Map<RegionInfo, Result>, Map<RegionInfo, Result>>
136    getMergedRegionsAndSplitParents() throws IOException {
137    return getMergedRegionsAndSplitParents(null);
138  }
139
140  /**
141   * Scans hbase:meta and returns a number of scanned rows, and a map of merged
142   * regions, and an ordered map of split parents. if the given table name is
143   * null, return merged regions and split parents of all tables, else only the
144   * specified table
145   * @param tableName null represents all tables
146   * @return triple of scanned rows, and map of merged regions, and map of split
147   *         parent regioninfos
148   * @throws IOException
149   */
150  Triple<Integer, Map<RegionInfo, Result>, Map<RegionInfo, Result>>
151    getMergedRegionsAndSplitParents(final TableName tableName) throws IOException {
152    final boolean isTableSpecified = (tableName != null);
153    // TODO: Only works with single hbase:meta region currently.  Fix.
154    final AtomicInteger count = new AtomicInteger(0);
155    // Keep Map of found split parents.  There are candidates for cleanup.
156    // Use a comparator that has split parents come before its daughters.
157    final Map<RegionInfo, Result> splitParents = new TreeMap<>(new SplitParentFirstComparator());
158    final Map<RegionInfo, Result> mergedRegions = new TreeMap<>(RegionInfo.COMPARATOR);
159    // This visitor collects split parents and counts rows in the hbase:meta table
160
161    MetaTableAccessor.Visitor visitor = new MetaTableAccessor.Visitor() {
162      @Override
163      public boolean visit(Result r) throws IOException {
164        if (r == null || r.isEmpty()) return true;
165        count.incrementAndGet();
166        RegionInfo info = MetaTableAccessor.getRegionInfo(r);
167        if (info == null) return true; // Keep scanning
168        if (isTableSpecified
169            && info.getTable().compareTo(tableName) > 0) {
170          // Another table, stop scanning
171          return false;
172        }
173        if (LOG.isTraceEnabled()) LOG.trace("" + info + " IS-SPLIT_PARENT=" + info.isSplitParent());
174        if (info.isSplitParent()) splitParents.put(info, r);
175        if (r.getValue(HConstants.CATALOG_FAMILY, HConstants.MERGEA_QUALIFIER) != null) {
176          mergedRegions.put(info, r);
177        }
178        // Returning true means "keep scanning"
179        return true;
180      }
181    };
182
183    // Run full scan of hbase:meta catalog table passing in our custom visitor with
184    // the start row
185    MetaTableAccessor.scanMetaForTableRegions(this.connection, visitor, tableName);
186
187    return new Triple<>(count.get(), mergedRegions, splitParents);
188  }
189
190  /**
191   * If merged region no longer holds reference to the merge regions, archive
192   * merge region on hdfs and perform deleting references in hbase:meta
193   * @param mergedRegion
194   * @return true if we delete references in merged region on hbase:meta and archive
195   *         the files on the file system
196   * @throws IOException
197   */
198  boolean cleanMergeRegion(final RegionInfo mergedRegion,
199      final RegionInfo regionA, final RegionInfo regionB) throws IOException {
200    FileSystem fs = this.services.getMasterFileSystem().getFileSystem();
201    Path rootdir = this.services.getMasterFileSystem().getRootDir();
202    Path tabledir = FSUtils.getTableDir(rootdir, mergedRegion.getTable());
203    TableDescriptor htd = getTableDescriptor(mergedRegion.getTable());
204    HRegionFileSystem regionFs = null;
205    try {
206      regionFs = HRegionFileSystem.openRegionFromFileSystem(
207          this.services.getConfiguration(), fs, tabledir, mergedRegion, true);
208    } catch (IOException e) {
209      LOG.warn("Merged region does not exist: " + mergedRegion.getEncodedName());
210    }
211    if (regionFs == null || !regionFs.hasReferences(htd)) {
212      LOG.debug("Deleting region " + regionA.getShortNameToLog() + " and "
213          + regionB.getShortNameToLog()
214          + " from fs because merged region no longer holds references");
215      ProcedureExecutor<MasterProcedureEnv> pe = this.services.getMasterProcedureExecutor();
216      pe.submitProcedure(new GCMergedRegionsProcedure(pe.getEnvironment(),
217          mergedRegion, regionA, regionB));
218      // Remove from in-memory states
219      this.services.getAssignmentManager().getRegionStates().deleteRegion(regionA);
220      this.services.getAssignmentManager().getRegionStates().deleteRegion(regionB);
221      this.services.getServerManager().removeRegion(regionA);
222      this.services.getServerManager().removeRegion(regionB);
223      return true;
224    }
225    return false;
226  }
227
228  /**
229   * Run janitorial scan of catalog <code>hbase:meta</code> table looking for
230   * garbage to collect.
231   * @return number of archiving jobs started.
232   * @throws IOException
233   */
234  int scan() throws IOException {
235    int result = 0;
236
237    try {
238      if (!alreadyRunning.compareAndSet(false, true)) {
239        LOG.debug("CatalogJanitor already running");
240        return result;
241      }
242      Triple<Integer, Map<RegionInfo, Result>, Map<RegionInfo, Result>> scanTriple =
243        getMergedRegionsAndSplitParents();
244      /**
245       * clean merge regions first
246       */
247      Map<RegionInfo, Result> mergedRegions = scanTriple.getSecond();
248      for (Map.Entry<RegionInfo, Result> e : mergedRegions.entrySet()) {
249        if (this.services.isInMaintenanceMode()) {
250          // Stop cleaning if the master is in maintenance mode
251          break;
252        }
253
254        PairOfSameType<RegionInfo> p = MetaTableAccessor.getMergeRegions(e.getValue());
255        RegionInfo regionA = p.getFirst();
256        RegionInfo regionB = p.getSecond();
257        if (regionA == null || regionB == null) {
258          LOG.warn("Unexpected references regionA="
259              + (regionA == null ? "null" : regionA.getShortNameToLog())
260              + ",regionB="
261              + (regionB == null ? "null" : regionB.getShortNameToLog())
262              + " in merged region " + e.getKey().getShortNameToLog());
263        } else {
264          if (cleanMergeRegion(e.getKey(), regionA, regionB)) {
265            result++;
266          }
267        }
268      }
269      /**
270       * clean split parents
271       */
272      Map<RegionInfo, Result> splitParents = scanTriple.getThird();
273
274      // Now work on our list of found parents. See if any we can clean up.
275      // regions whose parents are still around
276      HashSet<String> parentNotCleaned = new HashSet<>();
277      for (Map.Entry<RegionInfo, Result> e : splitParents.entrySet()) {
278        if (this.services.isInMaintenanceMode()) {
279          // Stop cleaning if the master is in maintenance mode
280          break;
281        }
282
283        if (!parentNotCleaned.contains(e.getKey().getEncodedName()) &&
284            cleanParent(e.getKey(), e.getValue())) {
285          result++;
286        } else {
287          // We could not clean the parent, so it's daughters should not be
288          // cleaned either (HBASE-6160)
289          PairOfSameType<RegionInfo> daughters =
290              MetaTableAccessor.getDaughterRegions(e.getValue());
291          parentNotCleaned.add(daughters.getFirst().getEncodedName());
292          parentNotCleaned.add(daughters.getSecond().getEncodedName());
293        }
294      }
295      return result;
296    } finally {
297      alreadyRunning.set(false);
298    }
299  }
300
301  /**
302   * Compare HRegionInfos in a way that has split parents sort BEFORE their
303   * daughters.
304   */
305  static class SplitParentFirstComparator implements Comparator<RegionInfo> {
306    Comparator<byte[]> rowEndKeyComparator = new Bytes.RowEndKeyComparator();
307    @Override
308    public int compare(RegionInfo left, RegionInfo right) {
309      // This comparator differs from the one RegionInfo in that it sorts
310      // parent before daughters.
311      if (left == null) return -1;
312      if (right == null) return 1;
313      // Same table name.
314      int result = left.getTable().compareTo(right.getTable());
315      if (result != 0) return result;
316      // Compare start keys.
317      result = Bytes.compareTo(left.getStartKey(), right.getStartKey());
318      if (result != 0) return result;
319      // Compare end keys, but flip the operands so parent comes first
320      result = rowEndKeyComparator.compare(right.getEndKey(), left.getEndKey());
321
322      return result;
323    }
324  }
325
326  /**
327   * If daughters no longer hold reference to the parents, delete the parent.
328   * @param parent RegionInfo of split offlined parent
329   * @param rowContent Content of <code>parent</code> row in
330   * <code>metaRegionName</code>
331   * @return True if we removed <code>parent</code> from meta table and from
332   * the filesystem.
333   * @throws IOException
334   */
335  boolean cleanParent(final RegionInfo parent, Result rowContent)
336  throws IOException {
337    // Check whether it is a merged region and not clean reference
338    // No necessary to check MERGEB_QUALIFIER because these two qualifiers will
339    // be inserted/deleted together
340    if (rowContent.getValue(HConstants.CATALOG_FAMILY, HConstants.MERGEA_QUALIFIER) != null) {
341      // wait cleaning merge region first
342      return false;
343    }
344    // Run checks on each daughter split.
345    PairOfSameType<RegionInfo> daughters = MetaTableAccessor.getDaughterRegions(rowContent);
346    Pair<Boolean, Boolean> a = checkDaughterInFs(parent, daughters.getFirst());
347    Pair<Boolean, Boolean> b = checkDaughterInFs(parent, daughters.getSecond());
348    if (hasNoReferences(a) && hasNoReferences(b)) {
349      String daughterA = daughters.getFirst() != null?
350          daughters.getFirst().getShortNameToLog(): "null";
351      String daughterB = daughters.getSecond() != null?
352          daughters.getSecond().getShortNameToLog(): "null";
353      LOG.debug("Deleting region " + parent.getShortNameToLog() +
354        " because daughters -- " + daughterA + ", " + daughterB +
355        " -- no longer hold references");
356      ProcedureExecutor<MasterProcedureEnv> pe = this.services.getMasterProcedureExecutor();
357      pe.submitProcedure(new GCRegionProcedure(pe.getEnvironment(), parent));
358      // Remove from in-memory states
359      this.services.getAssignmentManager().getRegionStates().deleteRegion(parent);
360      this.services.getServerManager().removeRegion(parent);
361      return true;
362    }
363    return false;
364  }
365
366  /**
367   * @param p A pair where the first boolean says whether or not the daughter
368   * region directory exists in the filesystem and then the second boolean says
369   * whether the daughter has references to the parent.
370   * @return True the passed <code>p</code> signifies no references.
371   */
372  private boolean hasNoReferences(final Pair<Boolean, Boolean> p) {
373    return !p.getFirst() || !p.getSecond();
374  }
375
376  /**
377   * Checks if a daughter region -- either splitA or splitB -- still holds
378   * references to parent.
379   * @param parent Parent region
380   * @param daughter Daughter region
381   * @return A pair where the first boolean says whether or not the daughter
382   * region directory exists in the filesystem and then the second boolean says
383   * whether the daughter has references to the parent.
384   * @throws IOException
385   */
386  Pair<Boolean, Boolean> checkDaughterInFs(final RegionInfo parent, final RegionInfo daughter)
387  throws IOException {
388    if (daughter == null)  {
389      return new Pair<>(Boolean.FALSE, Boolean.FALSE);
390    }
391
392    FileSystem fs = this.services.getMasterFileSystem().getFileSystem();
393    Path rootdir = this.services.getMasterFileSystem().getRootDir();
394    Path tabledir = FSUtils.getTableDir(rootdir, daughter.getTable());
395
396    Path daughterRegionDir = new Path(tabledir, daughter.getEncodedName());
397
398    HRegionFileSystem regionFs = null;
399
400    try {
401      if (!FSUtils.isExists(fs, daughterRegionDir)) {
402        return new Pair<>(Boolean.FALSE, Boolean.FALSE);
403      }
404    } catch (IOException ioe) {
405      LOG.error("Error trying to determine if daughter region exists, " +
406               "assuming exists and has references", ioe);
407      return new Pair<>(Boolean.TRUE, Boolean.TRUE);
408    }
409
410    boolean references = false;
411    TableDescriptor parentDescriptor = getTableDescriptor(parent.getTable());
412    try {
413      regionFs = HRegionFileSystem.openRegionFromFileSystem(
414          this.services.getConfiguration(), fs, tabledir, daughter, true);
415
416      for (ColumnFamilyDescriptor family: parentDescriptor.getColumnFamilies()) {
417        if ((references = regionFs.hasReferences(family.getNameAsString()))) {
418          break;
419        }
420      }
421    } catch (IOException e) {
422      LOG.error("Error trying to determine referenced files from : " + daughter.getEncodedName()
423          + ", to: " + parent.getEncodedName() + " assuming has references", e);
424      return new Pair<>(Boolean.TRUE, Boolean.TRUE);
425    }
426    return new Pair<>(Boolean.TRUE, Boolean.valueOf(references));
427  }
428
429  private TableDescriptor getTableDescriptor(final TableName tableName)
430      throws FileNotFoundException, IOException {
431    return this.services.getTableDescriptors().get(tableName);
432  }
433
434  /**
435   * Checks if the specified region has merge qualifiers, if so, try to clean
436   * them
437   * @param region
438   * @return true if the specified region doesn't have merge qualifier now
439   * @throws IOException
440   */
441  public boolean cleanMergeQualifier(final RegionInfo region)
442      throws IOException {
443    // Get merge regions if it is a merged region and already has merge
444    // qualifier
445    Pair<RegionInfo, RegionInfo> mergeRegions = MetaTableAccessor
446        .getRegionsFromMergeQualifier(this.services.getConnection(),
447          region.getRegionName());
448    if (mergeRegions == null
449        || (mergeRegions.getFirst() == null && mergeRegions.getSecond() == null)) {
450      // It doesn't have merge qualifier, no need to clean
451      return true;
452    }
453    // It shouldn't happen, we must insert/delete these two qualifiers together
454    if (mergeRegions.getFirst() == null || mergeRegions.getSecond() == null) {
455      LOG.error("Merged region " + region.getRegionNameAsString()
456          + " has only one merge qualifier in META.");
457      return false;
458    }
459    return cleanMergeRegion(region, mergeRegions.getFirst(),
460        mergeRegions.getSecond());
461  }
462}