All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.bigtable.beam.validation.ComputeAndValidateHashFromBigtableDoFn Maven / Gradle / Ivy

There is a newer version: 2.14.7
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.cloud.bigtable.beam.validation;

import static com.google.cloud.bigtable.beam.validation.SyncTableUtils.immutableBytesToString;

import com.google.bigtable.repackaged.com.google.common.base.Preconditions;
import com.google.bigtable.repackaged.com.google.common.collect.Lists;
import com.google.cloud.bigtable.beam.AbstractCloudBigtableTableDoFn;
import com.google.cloud.bigtable.beam.CloudBigtableConfiguration;
import com.google.cloud.bigtable.beam.TemplateUtils;
import com.google.cloud.bigtable.beam.validation.HadoopHashTableSource.RangeHash;
import com.google.cloud.bigtable.beam.validation.SyncTableJob.SyncTableOptions;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import org.apache.beam.sdk.metrics.Counter;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.values.KV;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.BigtableTableHashAccessor.BigtableResultHasher;

/**
 * A {@link DoFn} that takes a row range and hash from HBase and validates the hash from rows read
 * from Cloud Bigtable.
 */
class ComputeAndValidateHashFromBigtableDoFn
    extends AbstractCloudBigtableTableDoFn>>, RangeHash> {

  private static final long serialVersionUID = 2349094L;
  private final ValueProvider tableName;
  private final ValueProvider projectId;
  private final ValueProvider sourceHashDir;

  private final TableHashWrapperFactory tableHashWrapperFactory;

  // Counter for reporting matching and mismatching ranges. Names are similar to HBase sync-table
  // job.
  private final Counter matches = Metrics.counter("cbt-dataflow-validate", "ranges_matched");
  private final Counter mismatches = Metrics.counter("cbt-dataflow-validate", "ranges_not_matched");

  public ComputeAndValidateHashFromBigtableDoFn(SyncTableOptions options) {
    super(TemplateUtils.buildSyncTableConfig(options));
    this.tableName = options.getBigtableTableId();
    // Create a local copy of ValueProviders, PipelineOptions are not serializable.
    projectId = options.getBigtableProject();
    sourceHashDir = options.getHashTableOutputDir();
    tableHashWrapperFactory = new TableHashWrapperFactory();
  }

  @VisibleForTesting
  ComputeAndValidateHashFromBigtableDoFn(
      CloudBigtableConfiguration config,
      ValueProvider tableName,
      ValueProvider projectId,
      ValueProvider sourceHashDir,
      TableHashWrapperFactory factory) {
    super(config);
    this.tableName = tableName;
    this.tableHashWrapperFactory = factory;
    this.sourceHashDir = projectId;
    this.projectId = sourceHashDir;
  }

  @ProcessElement
  public void processElement(ProcessContext context) throws Exception {
    List> wrapperdRangeHashes = Lists.newArrayList(context.element().getValue());
    // BufferedHadoopHashTableSource generates only 1 item per groupby key, key is startKey for the
    // Sorted ranges.
    Preconditions.checkState(
        wrapperdRangeHashes.size() == 1, "Can not have multiple entries for a key");
    List rangeHashes = wrapperdRangeHashes.get(0);
    Preconditions.checkState(!rangeHashes.isEmpty(), "Can not have empty ranges in DO_FN");

    // If a metric is not logged, it is absent from all the metrics (as opposed to being
    // 0). By logging a 0 value for the metrics we guarantee that they shows up on Dataflow UIs.
    mismatches.inc(0);
    matches.inc(0);

    ImmutableBytesWritable rangeStartInclusive = rangeHashes.get(0).startInclusive;
    ImmutableBytesWritable rangeEndExclusive =
        rangeHashes.get(rangeHashes.size() - 1).stopExclusive;

    BigtableResultHasher resultHasher = new BigtableResultHasher();
    resultHasher.startBatch(rangeStartInclusive);

    // Since all the row-ranges are sorted in HashTable's data files, 1 big scan can be used
    // to read all the row ranges. Parallelism is achieved by splitting the HashTable's data
    // files into smaller bundle of row-ranges in GroupBy.
    ResultScanner scanner =
        createBigtableScan(rangeStartInclusive.copyBytes(), rangeEndExclusive.copyBytes());

    Iterator rangeHashIterator = rangeHashes.iterator();
    long numRows = 0;

    RangeHash currentRangeHash = rangeHashIterator.next();

    // Process each row and validate hashes
    for (Result result : scanner) {
      numRows++;
      if (numRows % 10_000 == 0) {
        // Heartbeat in logs in case a large scan gets hung.
        DOFN_LOG.debug("Processed " + numRows + " rows ");
      }

      ImmutableBytesWritable rowKey = new ImmutableBytesWritable(result.getRow());

      // Check if the rowKey belongs to current range, if not keep iterating through the
      // rangeHashes until rowKey's range is found.
      while (!isWithinUpperBound(currentRangeHash.stopExclusive, rowKey)) {
        validateBatchHash(context, resultHasher, currentRangeHash);
        // THIS SHOULD NEVER HAPPEN. Bigtable is being scanned till the last
        // RangeHash.endKeyExclusive(), so bigtable's result should not outlast the
        // rangeHashes.
        Preconditions.checkState(
            rangeHashIterator.hasNext(),
            "Buffer reached to end while scan is still active at row : %s. "
                + "Affected Range: [%s, %s)."
                + immutableBytesToString(result.getRow())
                + immutableBytesToString(rangeStartInclusive)
                + immutableBytesToString(rangeEndExclusive));
        currentRangeHash = rangeHashIterator.next();
      }

      // Always Hash the current row.
      resultHasher.hashResult(result);
    }

    // Bigtable scan is finished at this point and rangeHashes may contain additional row ranges.
    // Last range will always be unverified as the range end is exclusive and
    // currentRow > rangeEndExclusive will never by true. Verify the last range.
    validateBatchHash(context, resultHasher, currentRangeHash);

    // If there are remaining ranges in the rangeHashes they all need to reported as mismatched as
    // there is nothing in Cloud Bigtable for those row ranges.
    // for (int i = bufferIndex; i < rangeHashes.size(); i++) {
    while (rangeHashIterator.hasNext()) {
      currentRangeHash = rangeHashIterator.next();
      reportMismatch(context, currentRangeHash);
    }

    DOFN_LOG.debug(
        "Finishing context by outputting {}  keys in range [{}, {}).",
        rangeHashes.size(),
        immutableBytesToString(rangeStartInclusive),
        immutableBytesToString(rangeEndExclusive));
  }

  private ResultScanner createBigtableScan(byte[] startKeyInclusive, byte[] stopKeyExclusive)
      throws IOException {
    Table table = getConnection().getTable(TableName.valueOf(tableName.get()));
    // Get the scan from TableHash, HashTable can be run to hash a small part of data (selected
    // column families, timestamp range, maxVersions etc), this scan allows us to fetch the same
    // data from Cloud Bigtable to match.
    TableHashWrapper tableHash =
        tableHashWrapperFactory.getTableHash(projectId.get(), sourceHashDir.get());
    Scan scan = tableHash.getScan();
    // Set the workitem boundaries on the scan.
    if (startKeyInclusive.length > 0) {
      scan.withStartRow(startKeyInclusive, true);
    }
    if (stopKeyExclusive.length > 0) {
      scan.withStopRow(stopKeyExclusive, false);
    }

    return table.getScanner(scan);
  }

  /**
   * Determines if row >= stopExclusive for a row range (start, stopExclusive). Empty stopExclusive
   * represents a range with no upper bound.
   */
  private static boolean isWithinUpperBound(
      ImmutableBytesWritable stopExclusive, ImmutableBytesWritable row) {
    return stopExclusive.equals(HConstants.EMPTY_END_ROW) || row.compareTo(stopExclusive) < 0;
  }

  private void validateBatchHash(
      ProcessContext context, BigtableResultHasher resultHasher, RangeHash currentRangeHash) {
    // The batch is always started, so its safe to finish the batch. If there were no rows, we will
    // get a hash for empty batch.
    resultHasher.finishBatch();
    if (!resultHasher.getBatchHash().equals(currentRangeHash.hash)) {
      reportMismatch(context, currentRangeHash);
    } else {
      matches.inc();
    }
    // Start a new batch
    resultHasher.startBatch(currentRangeHash.stopExclusive);
  }

  private void reportMismatch(ProcessContext context, RangeHash currentRangeHash) {
    mismatches.inc();
    DOFN_LOG.info(
        "MISMATCH ON RANGE [{}, {}).",
        immutableBytesToString(currentRangeHash.startInclusive),
        immutableBytesToString(currentRangeHash.stopExclusive));
    context.output(currentRangeHash);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy