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.mapreduce;
019
020import static org.apache.hadoop.hbase.regionserver.HStoreFile.BULKLOAD_TASK_KEY;
021import static org.apache.hadoop.hbase.regionserver.HStoreFile.BULKLOAD_TIME_KEY;
022import static org.apache.hadoop.hbase.regionserver.HStoreFile.EXCLUDE_FROM_MINOR_COMPACTION_KEY;
023import static org.apache.hadoop.hbase.regionserver.HStoreFile.MAJOR_COMPACTION_KEY;
024
025import java.io.IOException;
026import java.io.UnsupportedEncodingException;
027import java.net.InetSocketAddress;
028import java.net.URLDecoder;
029import java.net.URLEncoder;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.TreeMap;
036import java.util.TreeSet;
037import java.util.UUID;
038import java.util.function.Function;
039import java.util.stream.Collectors;
040
041import org.apache.commons.lang3.StringUtils;
042import org.apache.hadoop.conf.Configuration;
043import org.apache.hadoop.fs.FileSystem;
044import org.apache.hadoop.fs.Path;
045import org.apache.hadoop.hbase.Cell;
046import org.apache.hadoop.hbase.CellComparator;
047import org.apache.hadoop.hbase.CellUtil;
048import org.apache.hadoop.hbase.HConstants;
049import org.apache.hadoop.hbase.HRegionLocation;
050import org.apache.hadoop.hbase.HTableDescriptor;
051import org.apache.hadoop.hbase.KeyValue;
052import org.apache.hadoop.hbase.PrivateCellUtil;
053import org.apache.hadoop.hbase.TableName;
054import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
055import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
056import org.apache.hadoop.hbase.client.Connection;
057import org.apache.hadoop.hbase.client.ConnectionFactory;
058import org.apache.hadoop.hbase.client.Put;
059import org.apache.hadoop.hbase.client.RegionLocator;
060import org.apache.hadoop.hbase.client.Table;
061import org.apache.hadoop.hbase.client.TableDescriptor;
062import org.apache.hadoop.hbase.fs.HFileSystem;
063import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
064import org.apache.hadoop.hbase.io.compress.Compression;
065import org.apache.hadoop.hbase.io.compress.Compression.Algorithm;
066import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
067import org.apache.hadoop.hbase.io.hfile.CacheConfig;
068import org.apache.hadoop.hbase.io.hfile.HFile;
069import org.apache.hadoop.hbase.io.hfile.HFileContext;
070import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder;
071import org.apache.hadoop.hbase.io.hfile.HFileWriterImpl;
072import org.apache.hadoop.hbase.regionserver.BloomType;
073import org.apache.hadoop.hbase.regionserver.HStore;
074import org.apache.hadoop.hbase.regionserver.StoreFileWriter;
075import org.apache.hadoop.hbase.util.Bytes;
076import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
077import org.apache.hadoop.hbase.util.FSUtils;
078import org.apache.hadoop.hbase.util.MapReduceExtendedCell;
079import org.apache.hadoop.io.NullWritable;
080import org.apache.hadoop.io.SequenceFile;
081import org.apache.hadoop.io.Text;
082import org.apache.hadoop.mapreduce.Job;
083import org.apache.hadoop.mapreduce.OutputCommitter;
084import org.apache.hadoop.mapreduce.OutputFormat;
085import org.apache.hadoop.mapreduce.RecordWriter;
086import org.apache.hadoop.mapreduce.TaskAttemptContext;
087import org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter;
088import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
089import org.apache.hadoop.mapreduce.lib.partition.TotalOrderPartitioner;
090import org.apache.yetus.audience.InterfaceAudience;
091import org.slf4j.Logger;
092import org.slf4j.LoggerFactory;
093
094import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
095
096/**
097 * Writes HFiles. Passed Cells must arrive in order.
098 * Writes current time as the sequence id for the file. Sets the major compacted
099 * attribute on created @{link {@link HFile}s. Calling write(null,null) will forcibly roll
100 * all HFiles being written.
101 * <p>
102 * Using this class as part of a MapReduce job is best done
103 * using {@link #configureIncrementalLoad(Job, TableDescriptor, RegionLocator)}.
104 */
105@InterfaceAudience.Public
106public class HFileOutputFormat2
107    extends FileOutputFormat<ImmutableBytesWritable, Cell> {
108  private static final Logger LOG = LoggerFactory.getLogger(HFileOutputFormat2.class);
109  static class TableInfo {
110    private TableDescriptor tableDesctiptor;
111    private RegionLocator regionLocator;
112
113    public TableInfo(TableDescriptor tableDesctiptor, RegionLocator regionLocator) {
114      this.tableDesctiptor = tableDesctiptor;
115      this.regionLocator = regionLocator;
116    }
117
118    /**
119     * The modification for the returned HTD doesn't affect the inner TD.
120     * @return A clone of inner table descriptor
121     * @deprecated use {@link #getTableDescriptor}
122     */
123    @Deprecated
124    public HTableDescriptor getHTableDescriptor() {
125      return new HTableDescriptor(tableDesctiptor);
126    }
127
128    public TableDescriptor getTableDescriptor() {
129      return tableDesctiptor;
130    }
131
132    public RegionLocator getRegionLocator() {
133      return regionLocator;
134    }
135  }
136
137  protected static final byte[] tableSeparator = Bytes.toBytes(";");
138
139  protected static byte[] combineTableNameSuffix(byte[] tableName, byte[] suffix) {
140    return Bytes.add(tableName, tableSeparator, suffix);
141  }
142
143  // The following constants are private since these are used by
144  // HFileOutputFormat2 to internally transfer data between job setup and
145  // reducer run using conf.
146  // These should not be changed by the client.
147  static final String COMPRESSION_FAMILIES_CONF_KEY =
148      "hbase.hfileoutputformat.families.compression";
149  static final String BLOOM_TYPE_FAMILIES_CONF_KEY =
150      "hbase.hfileoutputformat.families.bloomtype";
151  static final String BLOCK_SIZE_FAMILIES_CONF_KEY =
152      "hbase.mapreduce.hfileoutputformat.blocksize";
153  static final String DATABLOCK_ENCODING_FAMILIES_CONF_KEY =
154      "hbase.mapreduce.hfileoutputformat.families.datablock.encoding";
155
156  // This constant is public since the client can modify this when setting
157  // up their conf object and thus refer to this symbol.
158  // It is present for backwards compatibility reasons. Use it only to
159  // override the auto-detection of datablock encoding.
160  public static final String DATABLOCK_ENCODING_OVERRIDE_CONF_KEY =
161      "hbase.mapreduce.hfileoutputformat.datablock.encoding";
162
163  /**
164   * Keep locality while generating HFiles for bulkload. See HBASE-12596
165   */
166  public static final String LOCALITY_SENSITIVE_CONF_KEY =
167      "hbase.bulkload.locality.sensitive.enabled";
168  private static final boolean DEFAULT_LOCALITY_SENSITIVE = true;
169  static final String OUTPUT_TABLE_NAME_CONF_KEY =
170      "hbase.mapreduce.hfileoutputformat.table.name";
171  static final String MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY =
172          "hbase.mapreduce.use.multi.table.hfileoutputformat";
173
174  public static final String STORAGE_POLICY_PROPERTY = HStore.BLOCK_STORAGE_POLICY_KEY;
175  public static final String STORAGE_POLICY_PROPERTY_CF_PREFIX = STORAGE_POLICY_PROPERTY + ".";
176
177  @Override
178  public RecordWriter<ImmutableBytesWritable, Cell> getRecordWriter(
179      final TaskAttemptContext context) throws IOException, InterruptedException {
180    return createRecordWriter(context, this.getOutputCommitter(context));
181  }
182
183  protected static byte[] getTableNameSuffixedWithFamily(byte[] tableName, byte[] family) {
184    return combineTableNameSuffix(tableName, family);
185  }
186
187  static <V extends Cell> RecordWriter<ImmutableBytesWritable, V>
188      createRecordWriter(final TaskAttemptContext context, final OutputCommitter committer)
189          throws IOException {
190
191    // Get the path of the temporary output file
192    final Path outputDir = ((FileOutputCommitter)committer).getWorkPath();
193    final Configuration conf = context.getConfiguration();
194    final boolean writeMultipleTables = conf.getBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, false) ;
195    final String writeTableNames = conf.get(OUTPUT_TABLE_NAME_CONF_KEY);
196    if (writeTableNames==null || writeTableNames.isEmpty()) {
197      throw new IllegalArgumentException("Configuration parameter " + OUTPUT_TABLE_NAME_CONF_KEY
198              + " cannot be empty");
199    }
200    final FileSystem fs = outputDir.getFileSystem(conf);
201    // These configs. are from hbase-*.xml
202    final long maxsize = conf.getLong(HConstants.HREGION_MAX_FILESIZE,
203        HConstants.DEFAULT_MAX_FILE_SIZE);
204    // Invented config.  Add to hbase-*.xml if other than default compression.
205    final String defaultCompressionStr = conf.get("hfile.compression",
206        Compression.Algorithm.NONE.getName());
207    final Algorithm defaultCompression = HFileWriterImpl
208        .compressionByName(defaultCompressionStr);
209    final boolean compactionExclude = conf.getBoolean(
210        "hbase.mapreduce.hfileoutputformat.compaction.exclude", false);
211
212    final Set<String> allTableNames = Arrays.stream(writeTableNames.split(
213            Bytes.toString(tableSeparator))).collect(Collectors.toSet());
214
215    // create a map from column family to the compression algorithm
216    final Map<byte[], Algorithm> compressionMap = createFamilyCompressionMap(conf);
217    final Map<byte[], BloomType> bloomTypeMap = createFamilyBloomTypeMap(conf);
218    final Map<byte[], Integer> blockSizeMap = createFamilyBlockSizeMap(conf);
219
220    String dataBlockEncodingStr = conf.get(DATABLOCK_ENCODING_OVERRIDE_CONF_KEY);
221    final Map<byte[], DataBlockEncoding> datablockEncodingMap
222        = createFamilyDataBlockEncodingMap(conf);
223    final DataBlockEncoding overriddenEncoding;
224    if (dataBlockEncodingStr != null) {
225      overriddenEncoding = DataBlockEncoding.valueOf(dataBlockEncodingStr);
226    } else {
227      overriddenEncoding = null;
228    }
229
230    return new RecordWriter<ImmutableBytesWritable, V>() {
231      // Map of families to writers and how much has been output on the writer.
232      private final Map<byte[], WriterLength> writers =
233              new TreeMap<>(Bytes.BYTES_COMPARATOR);
234      private byte[] previousRow = HConstants.EMPTY_BYTE_ARRAY;
235      private final long now = EnvironmentEdgeManager.currentTime();
236      private boolean rollRequested = false;
237
238      @Override
239      public void write(ImmutableBytesWritable row, V cell)
240          throws IOException {
241        Cell kv = cell;
242        // null input == user explicitly wants to flush
243        if (row == null && kv == null) {
244          rollWriters(null);
245          return;
246        }
247
248        byte[] rowKey = CellUtil.cloneRow(kv);
249        int length = (PrivateCellUtil.estimatedSerializedSizeOf(kv)) - Bytes.SIZEOF_INT;
250        byte[] family = CellUtil.cloneFamily(kv);
251        byte[] tableNameBytes = null;
252        if (writeMultipleTables) {
253          tableNameBytes = MultiTableHFileOutputFormat.getTableName(row.get());
254          if (!allTableNames.contains(Bytes.toString(tableNameBytes))) {
255            throw new IllegalArgumentException("TableName '" + Bytes.toString(tableNameBytes) +
256                    "' not" + " expected");
257          }
258        } else {
259          tableNameBytes = Bytes.toBytes(writeTableNames);
260        }
261        byte[] tableAndFamily = getTableNameSuffixedWithFamily(tableNameBytes, family);
262        WriterLength wl = this.writers.get(tableAndFamily);
263
264        // If this is a new column family, verify that the directory exists
265        if (wl == null) {
266          Path writerPath = null;
267          if (writeMultipleTables) {
268            writerPath = new Path(outputDir, new Path(Bytes.toString(tableNameBytes), Bytes
269                    .toString(family)));
270          }
271          else {
272            writerPath = new Path(outputDir, Bytes.toString(family));
273          }
274          fs.mkdirs(writerPath);
275          configureStoragePolicy(conf, fs, tableAndFamily, writerPath);
276        }
277
278        if (wl != null && wl.written + length >= maxsize) {
279          this.rollRequested = true;
280        }
281
282        // This can only happen once a row is finished though
283        if (rollRequested && Bytes.compareTo(this.previousRow, rowKey) != 0) {
284          rollWriters(wl);
285        }
286
287        // create a new WAL writer, if necessary
288        if (wl == null || wl.writer == null) {
289          if (conf.getBoolean(LOCALITY_SENSITIVE_CONF_KEY, DEFAULT_LOCALITY_SENSITIVE)) {
290            HRegionLocation loc = null;
291
292            String tableName = Bytes.toString(tableNameBytes);
293            if (tableName != null) {
294              try (Connection connection = ConnectionFactory.createConnection(conf);
295                     RegionLocator locator =
296                       connection.getRegionLocator(TableName.valueOf(tableName))) {
297                loc = locator.getRegionLocation(rowKey);
298              } catch (Throwable e) {
299                LOG.warn("There's something wrong when locating rowkey: " +
300                  Bytes.toString(rowKey) + " for tablename: " + tableName, e);
301                loc = null;
302              } }
303
304            if (null == loc) {
305              if (LOG.isTraceEnabled()) {
306                LOG.trace("failed to get region location, so use default writer for rowkey: " +
307                  Bytes.toString(rowKey));
308              }
309              wl = getNewWriter(tableNameBytes, family, conf, null);
310            } else {
311              if (LOG.isDebugEnabled()) {
312                LOG.debug("first rowkey: [" + Bytes.toString(rowKey) + "]");
313              }
314              InetSocketAddress initialIsa =
315                  new InetSocketAddress(loc.getHostname(), loc.getPort());
316              if (initialIsa.isUnresolved()) {
317                if (LOG.isTraceEnabled()) {
318                  LOG.trace("failed to resolve bind address: " + loc.getHostname() + ":"
319                      + loc.getPort() + ", so use default writer");
320                }
321                wl = getNewWriter(tableNameBytes, family, conf, null);
322              } else {
323                if (LOG.isDebugEnabled()) {
324                  LOG.debug("use favored nodes writer: " + initialIsa.getHostString());
325                }
326                wl = getNewWriter(tableNameBytes, family, conf, new InetSocketAddress[] { initialIsa
327                });
328              }
329            }
330          } else {
331            wl = getNewWriter(tableNameBytes, family, conf, null);
332          }
333        }
334
335        // we now have the proper WAL writer. full steam ahead
336        PrivateCellUtil.updateLatestStamp(cell, this.now);
337        wl.writer.append(kv);
338        wl.written += length;
339
340        // Copy the row so we know when a row transition.
341        this.previousRow = rowKey;
342      }
343
344      private void rollWriters(WriterLength writerLength) throws IOException {
345        if (writerLength != null) {
346          closeWriter(writerLength);
347        } else {
348          for (WriterLength wl : this.writers.values()) {
349            closeWriter(wl);
350          }
351        }
352        this.rollRequested = false;
353      }
354
355      private void closeWriter(WriterLength wl) throws IOException {
356        if (wl.writer != null) {
357          LOG.info(
358              "Writer=" + wl.writer.getPath() + ((wl.written == 0)? "": ", wrote=" + wl.written));
359          close(wl.writer);
360        }
361        wl.writer = null;
362        wl.written = 0;
363      }
364
365      /*
366       * Create a new StoreFile.Writer.
367       * @param family
368       * @return A WriterLength, containing a new StoreFile.Writer.
369       * @throws IOException
370       */
371      @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="BX_UNBOXING_IMMEDIATELY_REBOXED",
372          justification="Not important")
373      private WriterLength getNewWriter(byte[] tableName, byte[] family, Configuration
374              conf, InetSocketAddress[] favoredNodes) throws IOException {
375        byte[] tableAndFamily = getTableNameSuffixedWithFamily(tableName, family);
376        Path familydir = new Path(outputDir, Bytes.toString(family));
377        if (writeMultipleTables) {
378          familydir = new Path(outputDir,
379                  new Path(Bytes.toString(tableName), Bytes.toString(family)));
380        }
381        WriterLength wl = new WriterLength();
382        Algorithm compression = compressionMap.get(tableAndFamily);
383        compression = compression == null ? defaultCompression : compression;
384        BloomType bloomType = bloomTypeMap.get(tableAndFamily);
385        bloomType = bloomType == null ? BloomType.NONE : bloomType;
386        Integer blockSize = blockSizeMap.get(tableAndFamily);
387        blockSize = blockSize == null ? HConstants.DEFAULT_BLOCKSIZE : blockSize;
388        DataBlockEncoding encoding = overriddenEncoding;
389        encoding = encoding == null ? datablockEncodingMap.get(tableAndFamily) : encoding;
390        encoding = encoding == null ? DataBlockEncoding.NONE : encoding;
391        Configuration tempConf = new Configuration(conf);
392        tempConf.setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0.0f);
393        HFileContextBuilder contextBuilder = new HFileContextBuilder()
394                                    .withCompression(compression)
395                                    .withChecksumType(HStore.getChecksumType(conf))
396                                    .withBytesPerCheckSum(HStore.getBytesPerChecksum(conf))
397                                    .withBlockSize(blockSize);
398
399        if (HFile.getFormatVersion(conf) >= HFile.MIN_FORMAT_VERSION_WITH_TAGS) {
400          contextBuilder.withIncludesTags(true);
401        }
402
403        contextBuilder.withDataBlockEncoding(encoding);
404        HFileContext hFileContext = contextBuilder.build();
405        if (null == favoredNodes) {
406          wl.writer =
407              new StoreFileWriter.Builder(conf, new CacheConfig(tempConf), fs)
408                  .withOutputDir(familydir).withBloomType(bloomType)
409                  .withComparator(CellComparator.getInstance()).withFileContext(hFileContext).build();
410        } else {
411          wl.writer =
412              new StoreFileWriter.Builder(conf, new CacheConfig(tempConf), new HFileSystem(fs))
413                  .withOutputDir(familydir).withBloomType(bloomType)
414                  .withComparator(CellComparator.getInstance()).withFileContext(hFileContext)
415                  .withFavoredNodes(favoredNodes).build();
416        }
417
418        this.writers.put(tableAndFamily, wl);
419        return wl;
420      }
421
422      private void close(final StoreFileWriter w) throws IOException {
423        if (w != null) {
424          w.appendFileInfo(BULKLOAD_TIME_KEY,
425              Bytes.toBytes(System.currentTimeMillis()));
426          w.appendFileInfo(BULKLOAD_TASK_KEY,
427              Bytes.toBytes(context.getTaskAttemptID().toString()));
428          w.appendFileInfo(MAJOR_COMPACTION_KEY,
429              Bytes.toBytes(true));
430          w.appendFileInfo(EXCLUDE_FROM_MINOR_COMPACTION_KEY,
431              Bytes.toBytes(compactionExclude));
432          w.appendTrackedTimestampsToMetadata();
433          w.close();
434        }
435      }
436
437      @Override
438      public void close(TaskAttemptContext c)
439      throws IOException, InterruptedException {
440        for (WriterLength wl: this.writers.values()) {
441          close(wl.writer);
442        }
443      }
444    };
445  }
446
447  /**
448   * Configure block storage policy for CF after the directory is created.
449   */
450  static void configureStoragePolicy(final Configuration conf, final FileSystem fs,
451      byte[] tableAndFamily, Path cfPath) {
452    if (null == conf || null == fs || null == tableAndFamily || null == cfPath) {
453      return;
454    }
455
456    String policy =
457        conf.get(STORAGE_POLICY_PROPERTY_CF_PREFIX + Bytes.toString(tableAndFamily),
458          conf.get(STORAGE_POLICY_PROPERTY));
459    FSUtils.setStoragePolicy(fs, cfPath, policy);
460  }
461
462  /*
463   * Data structure to hold a Writer and amount of data written on it.
464   */
465  static class WriterLength {
466    long written = 0;
467    StoreFileWriter writer = null;
468  }
469
470  /**
471   * Return the start keys of all of the regions in this table,
472   * as a list of ImmutableBytesWritable.
473   */
474  private static List<ImmutableBytesWritable> getRegionStartKeys(List<RegionLocator> regionLocators,
475                                                                 boolean writeMultipleTables)
476          throws IOException {
477
478    ArrayList<ImmutableBytesWritable> ret = new ArrayList<>();
479    for(RegionLocator regionLocator : regionLocators)
480    {
481      TableName tableName = regionLocator.getName();
482      LOG.info("Looking up current regions for table " + tableName);
483      byte[][] byteKeys = regionLocator.getStartKeys();
484      for (byte[] byteKey : byteKeys) {
485        byte[] fullKey = byteKey; //HFileOutputFormat2 use case
486        if (writeMultipleTables)
487        {
488          //MultiTableHFileOutputFormat use case
489          fullKey = combineTableNameSuffix(tableName.getName(), byteKey);
490        }
491        if (LOG.isDebugEnabled()) {
492          LOG.debug("SplitPoint startkey for table [" + tableName + "]: [" + Bytes.toStringBinary
493                  (fullKey) + "]");
494        }
495        ret.add(new ImmutableBytesWritable(fullKey));
496      }
497    }
498    return ret;
499  }
500
501  /**
502   * Write out a {@link SequenceFile} that can be read by
503   * {@link TotalOrderPartitioner} that contains the split points in startKeys.
504   */
505  @SuppressWarnings("deprecation")
506  private static void writePartitions(Configuration conf, Path partitionsPath,
507      List<ImmutableBytesWritable> startKeys, boolean writeMultipleTables) throws IOException {
508    LOG.info("Writing partition information to " + partitionsPath);
509    if (startKeys.isEmpty()) {
510      throw new IllegalArgumentException("No regions passed");
511    }
512
513    // We're generating a list of split points, and we don't ever
514    // have keys < the first region (which has an empty start key)
515    // so we need to remove it. Otherwise we would end up with an
516    // empty reducer with index 0
517    TreeSet<ImmutableBytesWritable> sorted = new TreeSet<>(startKeys);
518    ImmutableBytesWritable first = sorted.first();
519    if (writeMultipleTables) {
520      first = new ImmutableBytesWritable(MultiTableHFileOutputFormat.getSuffix(sorted.first
521              ().get()));
522    }
523    if (!first.equals(HConstants.EMPTY_BYTE_ARRAY)) {
524      throw new IllegalArgumentException(
525          "First region of table should have empty start key. Instead has: "
526          + Bytes.toStringBinary(first.get()));
527    }
528    sorted.remove(sorted.first());
529
530    // Write the actual file
531    FileSystem fs = partitionsPath.getFileSystem(conf);
532    SequenceFile.Writer writer = SequenceFile.createWriter(
533      fs, conf, partitionsPath, ImmutableBytesWritable.class,
534      NullWritable.class);
535
536    try {
537      for (ImmutableBytesWritable startKey : sorted) {
538        writer.append(startKey, NullWritable.get());
539      }
540    } finally {
541      writer.close();
542    }
543  }
544
545  /**
546   * Configure a MapReduce Job to perform an incremental load into the given
547   * table. This
548   * <ul>
549   *   <li>Inspects the table to configure a total order partitioner</li>
550   *   <li>Uploads the partitions file to the cluster and adds it to the DistributedCache</li>
551   *   <li>Sets the number of reduce tasks to match the current number of regions</li>
552   *   <li>Sets the output key/value class to match HFileOutputFormat2's requirements</li>
553   *   <li>Sets the reducer up to perform the appropriate sorting (either KeyValueSortReducer or
554   *     PutSortReducer)</li>
555   * </ul>
556   * The user should be sure to set the map output value class to either KeyValue or Put before
557   * running this function.
558   */
559  public static void configureIncrementalLoad(Job job, Table table, RegionLocator regionLocator)
560      throws IOException {
561    configureIncrementalLoad(job, table.getDescriptor(), regionLocator);
562  }
563
564  /**
565   * Configure a MapReduce Job to perform an incremental load into the given
566   * table. This
567   * <ul>
568   *   <li>Inspects the table to configure a total order partitioner</li>
569   *   <li>Uploads the partitions file to the cluster and adds it to the DistributedCache</li>
570   *   <li>Sets the number of reduce tasks to match the current number of regions</li>
571   *   <li>Sets the output key/value class to match HFileOutputFormat2's requirements</li>
572   *   <li>Sets the reducer up to perform the appropriate sorting (either KeyValueSortReducer or
573   *     PutSortReducer)</li>
574   * </ul>
575   * The user should be sure to set the map output value class to either KeyValue or Put before
576   * running this function.
577   */
578  public static void configureIncrementalLoad(Job job, TableDescriptor tableDescriptor,
579      RegionLocator regionLocator) throws IOException {
580    ArrayList<TableInfo> singleTableInfo = new ArrayList<>();
581    singleTableInfo.add(new TableInfo(tableDescriptor, regionLocator));
582    configureIncrementalLoad(job, singleTableInfo, HFileOutputFormat2.class);
583  }
584
585  static void configureIncrementalLoad(Job job, List<TableInfo> multiTableInfo,
586      Class<? extends OutputFormat<?, ?>> cls) throws IOException {
587    Configuration conf = job.getConfiguration();
588    job.setOutputKeyClass(ImmutableBytesWritable.class);
589    job.setOutputValueClass(MapReduceExtendedCell.class);
590    job.setOutputFormatClass(cls);
591
592    if (multiTableInfo.stream().distinct().count() != multiTableInfo.size()) {
593      throw new IllegalArgumentException("Duplicate entries found in TableInfo argument");
594    }
595    boolean writeMultipleTables = false;
596    if (MultiTableHFileOutputFormat.class.equals(cls)) {
597      writeMultipleTables = true;
598      conf.setBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, true);
599    }
600    // Based on the configured map output class, set the correct reducer to properly
601    // sort the incoming values.
602    // TODO it would be nice to pick one or the other of these formats.
603    if (KeyValue.class.equals(job.getMapOutputValueClass())
604        || MapReduceExtendedCell.class.equals(job.getMapOutputValueClass())) {
605      job.setReducerClass(CellSortReducer.class);
606    } else if (Put.class.equals(job.getMapOutputValueClass())) {
607      job.setReducerClass(PutSortReducer.class);
608    } else if (Text.class.equals(job.getMapOutputValueClass())) {
609      job.setReducerClass(TextSortReducer.class);
610    } else {
611      LOG.warn("Unknown map output value type:" + job.getMapOutputValueClass());
612    }
613
614    conf.setStrings("io.serializations", conf.get("io.serializations"),
615        MutationSerialization.class.getName(), ResultSerialization.class.getName(),
616        CellSerialization.class.getName());
617
618    if (conf.getBoolean(LOCALITY_SENSITIVE_CONF_KEY, DEFAULT_LOCALITY_SENSITIVE)) {
619      LOG.info("bulkload locality sensitive enabled");
620    }
621
622    /* Now get the region start keys for every table required */
623    List<String> allTableNames = new ArrayList<>(multiTableInfo.size());
624    List<RegionLocator> regionLocators = new ArrayList<>( multiTableInfo.size());
625    List<TableDescriptor> tableDescriptors = new ArrayList<>( multiTableInfo.size());
626
627    for( TableInfo tableInfo : multiTableInfo )
628    {
629      regionLocators.add(tableInfo.getRegionLocator());
630      allTableNames.add(tableInfo.getRegionLocator().getName().getNameAsString());
631      tableDescriptors.add(tableInfo.getTableDescriptor());
632    }
633    // Record tablenames for creating writer by favored nodes, and decoding compression, block size and other attributes of columnfamily per table
634    conf.set(OUTPUT_TABLE_NAME_CONF_KEY, StringUtils.join(allTableNames, Bytes
635            .toString(tableSeparator)));
636    List<ImmutableBytesWritable> startKeys = getRegionStartKeys(regionLocators, writeMultipleTables);
637    // Use table's region boundaries for TOP split points.
638    LOG.info("Configuring " + startKeys.size() + " reduce partitions " +
639        "to match current region count for all tables");
640    job.setNumReduceTasks(startKeys.size());
641
642    configurePartitioner(job, startKeys, writeMultipleTables);
643    // Set compression algorithms based on column families
644
645    conf.set(COMPRESSION_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(compressionDetails,
646            tableDescriptors));
647    conf.set(BLOCK_SIZE_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(blockSizeDetails,
648            tableDescriptors));
649    conf.set(BLOOM_TYPE_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(bloomTypeDetails,
650            tableDescriptors));
651    conf.set(DATABLOCK_ENCODING_FAMILIES_CONF_KEY,
652            serializeColumnFamilyAttribute(dataBlockEncodingDetails, tableDescriptors));
653
654    TableMapReduceUtil.addDependencyJars(job);
655    TableMapReduceUtil.initCredentials(job);
656    LOG.info("Incremental output configured for tables: " + StringUtils.join(allTableNames, ","));
657  }
658
659  public static void configureIncrementalLoadMap(Job job, TableDescriptor tableDescriptor) throws
660      IOException {
661    Configuration conf = job.getConfiguration();
662
663    job.setOutputKeyClass(ImmutableBytesWritable.class);
664    job.setOutputValueClass(MapReduceExtendedCell.class);
665    job.setOutputFormatClass(HFileOutputFormat2.class);
666
667    ArrayList<TableDescriptor> singleTableDescriptor = new ArrayList<>(1);
668    singleTableDescriptor.add(tableDescriptor);
669
670    conf.set(OUTPUT_TABLE_NAME_CONF_KEY, tableDescriptor.getTableName().getNameAsString());
671    // Set compression algorithms based on column families
672    conf.set(COMPRESSION_FAMILIES_CONF_KEY,
673        serializeColumnFamilyAttribute(compressionDetails, singleTableDescriptor));
674    conf.set(BLOCK_SIZE_FAMILIES_CONF_KEY,
675        serializeColumnFamilyAttribute(blockSizeDetails, singleTableDescriptor));
676    conf.set(BLOOM_TYPE_FAMILIES_CONF_KEY,
677        serializeColumnFamilyAttribute(bloomTypeDetails, singleTableDescriptor));
678    conf.set(DATABLOCK_ENCODING_FAMILIES_CONF_KEY,
679        serializeColumnFamilyAttribute(dataBlockEncodingDetails, singleTableDescriptor));
680
681    TableMapReduceUtil.addDependencyJars(job);
682    TableMapReduceUtil.initCredentials(job);
683    LOG.info("Incremental table " + tableDescriptor.getTableName() + " output configured.");
684  }
685
686  /**
687   * Runs inside the task to deserialize column family to compression algorithm
688   * map from the configuration.
689   *
690   * @param conf to read the serialized values from
691   * @return a map from column family to the configured compression algorithm
692   */
693  @VisibleForTesting
694  static Map<byte[], Algorithm> createFamilyCompressionMap(Configuration
695      conf) {
696    Map<byte[], String> stringMap = createFamilyConfValueMap(conf,
697        COMPRESSION_FAMILIES_CONF_KEY);
698    Map<byte[], Algorithm> compressionMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
699    for (Map.Entry<byte[], String> e : stringMap.entrySet()) {
700      Algorithm algorithm = HFileWriterImpl.compressionByName(e.getValue());
701      compressionMap.put(e.getKey(), algorithm);
702    }
703    return compressionMap;
704  }
705
706  /**
707   * Runs inside the task to deserialize column family to bloom filter type
708   * map from the configuration.
709   *
710   * @param conf to read the serialized values from
711   * @return a map from column family to the the configured bloom filter type
712   */
713  @VisibleForTesting
714  static Map<byte[], BloomType> createFamilyBloomTypeMap(Configuration conf) {
715    Map<byte[], String> stringMap = createFamilyConfValueMap(conf,
716        BLOOM_TYPE_FAMILIES_CONF_KEY);
717    Map<byte[], BloomType> bloomTypeMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
718    for (Map.Entry<byte[], String> e : stringMap.entrySet()) {
719      BloomType bloomType = BloomType.valueOf(e.getValue());
720      bloomTypeMap.put(e.getKey(), bloomType);
721    }
722    return bloomTypeMap;
723  }
724
725  /**
726   * Runs inside the task to deserialize column family to block size
727   * map from the configuration.
728   *
729   * @param conf to read the serialized values from
730   * @return a map from column family to the configured block size
731   */
732  @VisibleForTesting
733  static Map<byte[], Integer> createFamilyBlockSizeMap(Configuration conf) {
734    Map<byte[], String> stringMap = createFamilyConfValueMap(conf,
735        BLOCK_SIZE_FAMILIES_CONF_KEY);
736    Map<byte[], Integer> blockSizeMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
737    for (Map.Entry<byte[], String> e : stringMap.entrySet()) {
738      Integer blockSize = Integer.parseInt(e.getValue());
739      blockSizeMap.put(e.getKey(), blockSize);
740    }
741    return blockSizeMap;
742  }
743
744  /**
745   * Runs inside the task to deserialize column family to data block encoding
746   * type map from the configuration.
747   *
748   * @param conf to read the serialized values from
749   * @return a map from column family to HFileDataBlockEncoder for the
750   *         configured data block type for the family
751   */
752  @VisibleForTesting
753  static Map<byte[], DataBlockEncoding> createFamilyDataBlockEncodingMap(
754      Configuration conf) {
755    Map<byte[], String> stringMap = createFamilyConfValueMap(conf,
756        DATABLOCK_ENCODING_FAMILIES_CONF_KEY);
757    Map<byte[], DataBlockEncoding> encoderMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
758    for (Map.Entry<byte[], String> e : stringMap.entrySet()) {
759      encoderMap.put(e.getKey(), DataBlockEncoding.valueOf((e.getValue())));
760    }
761    return encoderMap;
762  }
763
764
765  /**
766   * Run inside the task to deserialize column family to given conf value map.
767   *
768   * @param conf to read the serialized values from
769   * @param confName conf key to read from the configuration
770   * @return a map of column family to the given configuration value
771   */
772  private static Map<byte[], String> createFamilyConfValueMap(
773      Configuration conf, String confName) {
774    Map<byte[], String> confValMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
775    String confVal = conf.get(confName, "");
776    for (String familyConf : confVal.split("&")) {
777      String[] familySplit = familyConf.split("=");
778      if (familySplit.length != 2) {
779        continue;
780      }
781      try {
782        confValMap.put(Bytes.toBytes(URLDecoder.decode(familySplit[0], "UTF-8")),
783            URLDecoder.decode(familySplit[1], "UTF-8"));
784      } catch (UnsupportedEncodingException e) {
785        // will not happen with UTF-8 encoding
786        throw new AssertionError(e);
787      }
788    }
789    return confValMap;
790  }
791
792  /**
793   * Configure <code>job</code> with a TotalOrderPartitioner, partitioning against
794   * <code>splitPoints</code>. Cleans up the partitions file after job exists.
795   */
796  static void configurePartitioner(Job job, List<ImmutableBytesWritable> splitPoints, boolean
797          writeMultipleTables)
798      throws IOException {
799    Configuration conf = job.getConfiguration();
800    // create the partitions file
801    FileSystem fs = FileSystem.get(conf);
802    String hbaseTmpFsDir =
803        conf.get(HConstants.TEMPORARY_FS_DIRECTORY_KEY,
804          HConstants.DEFAULT_TEMPORARY_HDFS_DIRECTORY);
805    Path partitionsPath = new Path(hbaseTmpFsDir, "partitions_" + UUID.randomUUID());
806    fs.makeQualified(partitionsPath);
807    writePartitions(conf, partitionsPath, splitPoints, writeMultipleTables);
808    fs.deleteOnExit(partitionsPath);
809
810    // configure job to use it
811    job.setPartitionerClass(TotalOrderPartitioner.class);
812    TotalOrderPartitioner.setPartitionFile(conf, partitionsPath);
813  }
814
815  @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
816  @VisibleForTesting
817  static String serializeColumnFamilyAttribute(Function<ColumnFamilyDescriptor, String> fn, List<TableDescriptor> allTables)
818      throws UnsupportedEncodingException {
819    StringBuilder attributeValue = new StringBuilder();
820    int i = 0;
821    for (TableDescriptor tableDescriptor : allTables) {
822      if (tableDescriptor == null) {
823        // could happen with mock table instance
824        // CODEREVIEW: Can I set an empty string in conf if mock table instance?
825        return "";
826      }
827      for (ColumnFamilyDescriptor familyDescriptor : tableDescriptor.getColumnFamilies()) {
828        if (i++ > 0) {
829          attributeValue.append('&');
830        }
831        attributeValue.append(URLEncoder.encode(
832            Bytes.toString(combineTableNameSuffix(tableDescriptor.getTableName().getName(), familyDescriptor.getName())),
833            "UTF-8"));
834        attributeValue.append('=');
835        attributeValue.append(URLEncoder.encode(fn.apply(familyDescriptor), "UTF-8"));
836      }
837    }
838    // Get rid of the last ampersand
839    return attributeValue.toString();
840  }
841
842  /**
843   * Serialize column family to compression algorithm map to configuration.
844   * Invoked while configuring the MR job for incremental load.
845   *
846   * @param tableDescriptor to read the properties from
847   * @param conf to persist serialized values into
848   * @throws IOException
849   *           on failure to read column family descriptors
850   */
851  @VisibleForTesting
852  static Function<ColumnFamilyDescriptor, String> compressionDetails = familyDescriptor ->
853          familyDescriptor.getCompressionType().getName();
854
855  /**
856   * Serialize column family to block size map to configuration. Invoked while
857   * configuring the MR job for incremental load.
858   *
859   * @param tableDescriptor
860   *          to read the properties from
861   * @param conf
862   *          to persist serialized values into
863   *
864   * @throws IOException
865   *           on failure to read column family descriptors
866   */
867  @VisibleForTesting
868  static Function<ColumnFamilyDescriptor, String> blockSizeDetails = familyDescriptor -> String
869          .valueOf(familyDescriptor.getBlocksize());
870
871  /**
872   * Serialize column family to bloom type map to configuration. Invoked while
873   * configuring the MR job for incremental load.
874   *
875   * @param tableDescriptor
876   *          to read the properties from
877   * @param conf
878   *          to persist serialized values into
879   *
880   * @throws IOException
881   *           on failure to read column family descriptors
882   */
883  @VisibleForTesting
884  static Function<ColumnFamilyDescriptor, String> bloomTypeDetails = familyDescriptor -> {
885    String bloomType = familyDescriptor.getBloomFilterType().toString();
886    if (bloomType == null) {
887      bloomType = ColumnFamilyDescriptorBuilder.DEFAULT_BLOOMFILTER.name();
888    }
889    return bloomType;
890  };
891
892  /**
893   * Serialize column family to data block encoding map to configuration.
894   * Invoked while configuring the MR job for incremental load.
895   *
896   * @param tableDescriptor
897   *          to read the properties from
898   * @param conf
899   *          to persist serialized values into
900   * @throws IOException
901   *           on failure to read column family descriptors
902   */
903  @VisibleForTesting
904  static Function<ColumnFamilyDescriptor, String> dataBlockEncodingDetails = familyDescriptor -> {
905    DataBlockEncoding encoding = familyDescriptor.getDataBlockEncoding();
906    if (encoding == null) {
907      encoding = DataBlockEncoding.NONE;
908    }
909    return encoding.toString();
910  };
911
912}