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.backup.util;
019
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.Map;
023import org.apache.hadoop.fs.Path;
024import org.apache.hadoop.hbase.net.Address;
025import org.apache.hadoop.hbase.wal.AbstractFSWALProvider;
026import org.apache.yetus.audience.InterfaceAudience;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030/**
031 * Tracks time boundaries for WAL file cleanup during backup operations. Maintains the oldest
032 * timestamp per RegionServer included in any backup, enabling safe determination of which WAL files
033 * can be deleted without compromising backup integrity.
034 */
035@InterfaceAudience.Private
036public class BackupBoundaries {
037  private static final Logger LOG = LoggerFactory.getLogger(BackupBoundaries.class);
038  private static final BackupBoundaries EMPTY_BOUNDARIES =
039    new BackupBoundaries(Collections.emptyMap(), Long.MAX_VALUE);
040
041  // This map tracks, for every RegionServer, the least recent (= oldest / lowest timestamp)
042  // inclusion in any backup. In other words, it is the timestamp boundary up to which all backup
043  // roots have included the WAL in their backup.
044  private final Map<Address, Long> boundaries;
045
046  // The minimum WAL roll timestamp from the most recent backup of each backup root, used as a
047  // fallback cleanup boundary for RegionServers without explicit backup boundaries (e.g., servers
048  // that joined after backups began)
049  private final long oldestStartCode;
050
051  private BackupBoundaries(Map<Address, Long> boundaries, long oldestStartCode) {
052    this.boundaries = boundaries;
053    this.oldestStartCode = oldestStartCode;
054  }
055
056  public boolean isDeletable(Path walLogPath) {
057    try {
058      String hostname = BackupUtils.parseHostNameFromLogFile(walLogPath);
059
060      if (hostname == null) {
061        LOG.warn(
062          "Cannot parse hostname from RegionServer WAL file: {}. Ignoring cleanup of this log",
063          walLogPath);
064        return false;
065      }
066
067      Address address = Address.fromString(hostname);
068      long pathTs = AbstractFSWALProvider.getTimestamp(walLogPath.getName());
069
070      if (!boundaries.containsKey(address)) {
071        boolean isDeletable = pathTs <= oldestStartCode;
072        if (LOG.isDebugEnabled()) {
073          LOG.debug(
074            "Boundary for {} not found. isDeletable = {} based on oldestStartCode = {} and WAL ts of {}",
075            walLogPath, isDeletable, oldestStartCode, pathTs);
076        }
077        return isDeletable;
078      }
079
080      long backupTs = boundaries.get(address);
081      if (pathTs <= backupTs) {
082        if (LOG.isDebugEnabled()) {
083          LOG.debug(
084            "WAL cleanup time-boundary found for server {}: {}. Ok to delete older file: {}",
085            address.getHostName(), pathTs, walLogPath);
086        }
087        return true;
088      }
089
090      if (LOG.isDebugEnabled()) {
091        LOG.debug("WAL cleanup time-boundary found for server {}: {}. Keeping younger file: {}",
092          address.getHostName(), backupTs, walLogPath);
093      }
094
095      return false;
096    } catch (Exception e) {
097      LOG.warn("Error occurred while filtering file: {}. Ignoring cleanup of this log", walLogPath,
098        e);
099      return false;
100    }
101  }
102
103  public Map<Address, Long> getBoundaries() {
104    return boundaries;
105  }
106
107  public long getOldestStartCode() {
108    return oldestStartCode;
109  }
110
111  public static BackupBoundariesBuilder builder(long tsCleanupBuffer) {
112    return new BackupBoundariesBuilder(tsCleanupBuffer);
113  }
114
115  public static class BackupBoundariesBuilder {
116    private final Map<Address, Long> boundaries = new HashMap<>();
117    private final long tsCleanupBuffer;
118
119    private long oldestStartCode = Long.MAX_VALUE;
120
121    private BackupBoundariesBuilder(long tsCleanupBuffer) {
122      this.tsCleanupBuffer = tsCleanupBuffer;
123    }
124
125    public BackupBoundariesBuilder addBackupTimestamps(String host, long hostLogRollTs,
126      long backupStartCode) {
127      Address address = Address.fromString(host);
128      Long storedTs = boundaries.get(address);
129      if (storedTs == null || hostLogRollTs < storedTs) {
130        boundaries.put(address, hostLogRollTs);
131      }
132
133      if (oldestStartCode > backupStartCode) {
134        oldestStartCode = backupStartCode;
135      }
136
137      return this;
138    }
139
140    public BackupBoundaries build() {
141      if (boundaries.isEmpty()) {
142        return EMPTY_BOUNDARIES;
143      }
144
145      oldestStartCode -= tsCleanupBuffer;
146      return new BackupBoundaries(boundaries, oldestStartCode);
147    }
148  }
149}