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.mapreduce;
019
020import static org.apache.hadoop.hbase.mapreduce.WALPlayer.TABLES_KEY;
021import static org.apache.hadoop.hbase.mapreduce.WALPlayer.TABLE_MAP_KEY;
022
023import java.io.IOException;
024import java.text.ParseException;
025import java.text.SimpleDateFormat;
026import java.util.List;
027import java.util.Map;
028import java.util.TreeMap;
029import org.apache.hadoop.conf.Configuration;
030import org.apache.hadoop.conf.Configured;
031import org.apache.hadoop.fs.Path;
032import org.apache.hadoop.hbase.HBaseConfiguration;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.backup.util.BackupFileSystemManager;
035import org.apache.hadoop.hbase.backup.util.BulkLoadProcessor;
036import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
037import org.apache.hadoop.hbase.mapreduce.WALInputFormat;
038import org.apache.hadoop.hbase.regionserver.wal.WALCellCodec;
039import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
040import org.apache.hadoop.hbase.wal.WALEdit;
041import org.apache.hadoop.hbase.wal.WALKey;
042import org.apache.hadoop.io.NullWritable;
043import org.apache.hadoop.io.Text;
044import org.apache.hadoop.mapreduce.Job;
045import org.apache.hadoop.mapreduce.Mapper;
046import org.apache.hadoop.mapreduce.Reducer;
047import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
048import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
049import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
050import org.apache.hadoop.util.Tool;
051import org.apache.hadoop.util.ToolRunner;
052import org.apache.yetus.audience.InterfaceAudience;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056/**
057 * MapReduce job that scans WAL backups and extracts referenced bulk-load store-file paths.
058 * <p>
059 * This job is intended to be used when you want a list of HFiles / store-files referenced by WAL
060 * bulk-load descriptors. It emits a de-duplicated list of full paths (one per line) by default
061 * using the {@link DedupReducer}.
062 * </p>
063 * <p>
064 * Usage (CLI):
065 * {@code BulkLoadCollector <WAL inputdir> <bulk-files-output-dir> [<tables> [<tableMappings>]]}
066 * </p>
067 */
068@InterfaceAudience.Private
069public class BulkLoadCollectorJob extends Configured implements Tool {
070  private static final Logger LOG = LoggerFactory.getLogger(BulkLoadCollectorJob.class);
071
072  public static final String NAME = "BulkLoadCollector";
073  public static final String DEFAULT_REDUCERS = "1";
074
075  public BulkLoadCollectorJob() {
076  }
077
078  public BulkLoadCollectorJob(final Configuration c) {
079    super(c);
080  }
081
082  /**
083   * Mapper that extracts relative bulk-load paths from a WAL entry (via {@code BulkLoadProcessor}),
084   * resolves them to full paths (via
085   * {@code BackupFileSystemManager#resolveBulkLoadFullPath(Path, Path)}), and emits each full path
086   * as the map key (Text). Uses the same table-filtering semantics as other WAL mappers: if no
087   * tables are configured, all tables are processed; otherwise only the configured table set is
088   * processed. Map output: (Text fullPathString, NullWritable)
089   */
090  public static class BulkLoadCollectorMapper extends Mapper<WALKey, WALEdit, Text, NullWritable> {
091    private final Map<TableName, TableName> tables = new TreeMap<>();
092    private final Text out = new Text();
093
094    @Override
095    protected void map(WALKey key, WALEdit value, Context context)
096      throws IOException, InterruptedException {
097      if (key == null) {
098        if (LOG.isTraceEnabled()) LOG.trace("map: received null WALKey, skipping");
099        return;
100      }
101      if (value == null) {
102        if (LOG.isTraceEnabled())
103          LOG.trace("map: received null WALEdit for table={}, skipping", safeTable(key));
104        return;
105      }
106
107      TableName tname = key.getTableName();
108
109      // table filtering
110      if (!(tables.isEmpty() || tables.containsKey(tname))) {
111        if (LOG.isTraceEnabled()) {
112          LOG.trace("map: skipping table={} because it is not in configured table list", tname);
113        }
114        return;
115      }
116
117      // Extract relative store-file paths referenced by this WALEdit.
118      // Delegates parsing to BulkLoadProcessor so parsing logic is centralized.
119      List<Path> relativePaths = BulkLoadProcessor.processBulkLoadFiles(key, value);
120      if (relativePaths.isEmpty()) return;
121
122      // Determine WAL input path for this split (used to compute date/prefix for full path)
123      Path walInputPath;
124      try {
125        walInputPath =
126          new Path(((WALInputFormat.WALSplit) context.getInputSplit()).getLogFileName());
127      } catch (ClassCastException cce) {
128        String splitClass =
129          (context.getInputSplit() == null) ? "null" : context.getInputSplit().getClass().getName();
130        LOG.warn(
131          "map: unexpected InputSplit type (not WALSplit) - cannot determine WAL input path; context.getInputSplit() class={}",
132          splitClass, cce);
133        throw new IOException("Unexpected InputSplit type: expected WALSplit but got " + splitClass,
134          cce);
135      }
136
137      if (LOG.isTraceEnabled()) {
138        LOG.trace("map: walInputPath={} table={} relativePathsCount={}", walInputPath, tname,
139          relativePaths.size());
140      }
141
142      // Build full path for each relative path and emit it.
143      for (Path rel : relativePaths) {
144        Path full = BackupFileSystemManager.resolveBulkLoadFullPath(walInputPath, rel);
145        out.set(full.toString());
146        context.write(out, NullWritable.get());
147        context.getCounter("BulkCollector", "StoreFilesEmitted").increment(1);
148      }
149    }
150
151    @Override
152    protected void setup(Context context) throws IOException {
153      String[] tableMap = context.getConfiguration().getStrings(TABLE_MAP_KEY);
154      String[] tablesToUse = context.getConfiguration().getStrings(TABLES_KEY);
155      if (tableMap == null) {
156        tableMap = tablesToUse;
157      }
158      if (tablesToUse == null) {
159        // user requested all tables; tables map remains empty to indicate "all"
160        return;
161      }
162
163      if (tablesToUse.length != tableMap.length) {
164        throw new IOException("Incorrect table mapping specified.");
165      }
166
167      int i = 0;
168      for (String table : tablesToUse) {
169        TableName from = TableName.valueOf(table);
170        TableName to = TableName.valueOf(tableMap[i++]);
171        tables.put(from, to);
172        if (LOG.isDebugEnabled()) {
173          LOG.debug("setup: configuring mapping {} -> {}", from, to);
174        }
175      }
176    }
177
178    private String safeTable(WALKey key) {
179      try {
180        return key == null ? "<null>" : key.getTableName().toString();
181      } catch (Exception e) {
182        return "<error>";
183      }
184    }
185  }
186
187  /**
188   * Reducer that deduplicates full-path keys emitted by the mappers. It writes each unique key
189   * exactly once. Reduce input: (Text fullPathString, Iterable<NullWritable>) Reduce output: (Text
190   * fullPathString, NullWritable)
191   */
192  public static class DedupReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
193    @Override
194    protected void reduce(Text key, Iterable<NullWritable> values, Context ctx)
195      throws IOException, InterruptedException {
196      // Write the unique path once.
197      ctx.write(key, NullWritable.get());
198    }
199  }
200
201  /**
202   * Create and configure a Job instance for bulk-file collection.
203   * @param args CLI args expected to be: inputDirs bulkFilesOut [tables] [tableMap]
204   * @throws IOException on misconfiguration
205   */
206  public Job createSubmittableJob(String[] args) throws IOException {
207    Configuration conf = getConf();
208
209    setupTime(conf, WALInputFormat.START_TIME_KEY);
210    setupTime(conf, WALInputFormat.END_TIME_KEY);
211
212    if (args == null || args.length < 2) {
213      throw new IOException(
214        "Usage: <WAL inputdir> <bulk-files-output-dir> [<tables> [<tableMappings>]]");
215    }
216
217    String inputDirs = args[0];
218    String bulkFilesOut = args[1];
219
220    // tables are optional (args[2])
221    String[] tables = (args.length == 2) ? new String[] {} : args[2].split(",");
222    String[] tableMap;
223    if (args.length > 3) {
224      tableMap = args[3].split(",");
225      if (tableMap.length != tables.length) {
226        throw new IOException("The same number of tables and mapping must be provided.");
227      }
228    } else {
229      // if no mapping is specified, map each table to itself
230      tableMap = tables;
231    }
232
233    LOG.info("createSubmittableJob: inputDirs='{}' bulkFilesOut='{}' tables={} tableMap={}",
234      inputDirs, bulkFilesOut, String.join(",", tables), String.join(",", tableMap));
235
236    conf.setStrings(TABLES_KEY, tables);
237    conf.setStrings(TABLE_MAP_KEY, tableMap);
238    conf.set(FileInputFormat.INPUT_DIR, inputDirs);
239
240    // create and return the actual Job configured for bulk-file discovery
241    return BulkLoadCollectorJob.createSubmittableJob(conf, inputDirs, bulkFilesOut);
242  }
243
244  /**
245   * Low-level job wiring. Creates the Job instance and sets input, mapper, reducer and output.
246   * @param conf         configuration used for the job
247   * @param inputDirs    WAL input directories (comma-separated)
248   * @param bulkFilesOut output directory to write discovered full-paths
249   * @throws IOException on invalid args
250   */
251  private static Job createSubmittableJob(Configuration conf, String inputDirs, String bulkFilesOut)
252    throws IOException {
253    if (bulkFilesOut == null || bulkFilesOut.isEmpty()) {
254      throw new IOException("bulkFilesOut (output dir) must be provided.");
255    }
256    if (inputDirs == null || inputDirs.isEmpty()) {
257      throw new IOException("inputDirs (WAL input dir) must be provided.");
258    }
259
260    Job job = Job.getInstance(conf, NAME + "_" + EnvironmentEdgeManager.currentTime());
261    job.setJarByClass(BulkLoadCollectorJob.class);
262
263    // Input: use same WALInputFormat used by WALPlayer so we parse WALs consistently
264    job.setInputFormatClass(WALInputFormat.class);
265    FileInputFormat.setInputDirRecursive(job, true);
266    FileInputFormat.setInputPaths(job, inputDirs);
267
268    // Mapper: extract and emit full bulk-load file paths (Text, NullWritable)
269    job.setMapperClass(BulkLoadCollectorMapper.class);
270    job.setMapOutputKeyClass(Text.class);
271    job.setMapOutputValueClass(NullWritable.class);
272
273    // Reducer: deduplicate the full-path keys
274    job.setReducerClass(DedupReducer.class);
275    // default to a single reducer (single deduped file); callers can set mapreduce.job.reduces
276    int reducers = conf.getInt("mapreduce.job.reduces", Integer.parseInt(DEFAULT_REDUCERS));
277    job.setNumReduceTasks(reducers);
278
279    // Output: write plain text lines (one path per line)
280    job.setOutputFormatClass(TextOutputFormat.class);
281    FileOutputFormat.setOutputPath(job, new Path(bulkFilesOut));
282
283    LOG.info("createSubmittableJob: created job name='{}' reducers={}", job.getJobName(), reducers);
284
285    String codecCls = WALCellCodec.getWALCellCodecClass(conf).getName();
286    try {
287      TableMapReduceUtil.addDependencyJarsForClasses(job.getConfiguration(),
288        Class.forName(codecCls));
289    } catch (Exception e) {
290      throw new IOException("Cannot determine wal codec class " + codecCls, e);
291    }
292    return job;
293  }
294
295  /**
296   * Parse a time option. Supports the user-friendly ISO-like format
297   * {@code yyyy-MM-dd'T'HH:mm:ss.SS} or milliseconds since epoch. If the option is not present,
298   * this method is a no-op.
299   * @param conf   configuration containing option
300   * @param option key to read (e.g. WALInputFormat.START_TIME_KEY)
301   * @throws IOException on parse failure
302   */
303  private void setupTime(Configuration conf, String option) throws IOException {
304    String val = conf.get(option);
305    if (val == null) {
306      return;
307    }
308    long ms;
309    try {
310      // first try to parse in user-friendly form
311      ms = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").parse(val).getTime();
312    } catch (ParseException pe) {
313      try {
314        // then see if a number (milliseconds) was specified
315        ms = Long.parseLong(val);
316      } catch (NumberFormatException nfe) {
317        throw new IOException(
318          option + " must be specified either in the form 2001-02-20T16:35:06.99 "
319            + "or as number of milliseconds");
320      }
321    }
322    conf.setLong(option, ms);
323  }
324
325  /**
326   * CLI entry point.
327   * @param args job arguments (see {@link #usage(String)})
328   * @throws Exception on job failure
329   */
330  public static void main(String[] args) throws Exception {
331    int ret = ToolRunner.run(new BulkLoadCollectorJob(HBaseConfiguration.create()), args);
332    System.exit(ret);
333  }
334
335  @Override
336  public int run(String[] args) throws Exception {
337    if (args.length < 2) {
338      usage("Wrong number of arguments: " + args.length);
339      System.exit(-1);
340    }
341
342    Job job = createSubmittableJob(args);
343    return job.waitForCompletion(true) ? 0 : 1;
344  }
345
346  /**
347   * Print usage/help for the BulkLoadCollectorJob CLI/driver.
348   * <p>
349   *
350   * <pre>
351   * args layout:
352   *   args[0] = input directory (required)
353   *   args[1] = output directory (required)
354   *   args[2] = tables (comma-separated) (optional)
355   *   args[3] = tableMappings (comma-separated) (optional; must match tables length)
356   * </pre>
357   */
358  private void usage(final String errorMsg) {
359    if (errorMsg != null && !errorMsg.isEmpty()) {
360      System.err.println("ERROR: " + errorMsg);
361    }
362
363    System.err.println(
364      "Usage: " + NAME + " <WAL inputdir> <bulk-files-output-dir> [<tables> [<tableMappings>]]");
365    System.err.println(
366      "  <WAL inputdir>             directory of WALs to scan (comma-separated list accepted)");
367    System.err.println(
368      "  <bulk-files-output-dir>    directory to write discovered store-file paths (output)");
369    System.err.println(
370      "  <tables>                   optional comma-separated list of tables to include; if omitted, all tables are processed");
371    System.err.println(
372      "  <tableMappings>            optional comma-separated list of mapped target tables; must match number of tables");
373
374    System.err.println();
375    System.err.println("Time range options (either milliseconds or yyyy-MM-dd'T'HH:mm:ss.SS):");
376    System.err.println("  -D" + WALInputFormat.START_TIME_KEY + "=[date|ms]");
377    System.err.println("  -D" + WALInputFormat.END_TIME_KEY + "=[date|ms]");
378
379    System.err.println();
380    System.err.println("Configuration alternatives (can be provided via -D):");
381    System.err
382      .println("  -D" + TABLES_KEY + "=<comma-separated-tables>         (alternative to arg[2])");
383    System.err
384      .println("  -D" + TABLE_MAP_KEY + "=<comma-separated-mappings>     (alternative to arg[3])");
385    System.err.println(
386      "  -Dmapreduce.job.reduces=<N>                            (number of reducers; default 1)");
387    System.err.println();
388
389    System.err.println("Performance hints:");
390    System.err.println("  For large inputs consider disabling speculative execution:");
391    System.err
392      .println("    -Dmapreduce.map.speculative=false -Dmapreduce.reduce.speculative=false");
393
394    System.err.println();
395    System.err.println("Example:");
396    System.err.println(
397      "  " + NAME + " /wals/input /out/bulkfiles ns:tbl1,ns:tbl2 ns:tbl1_mapped,ns:tbl2_mapped");
398  }
399}