Package org.kiji.schema.layout.impl.cassandra

Source Code of org.kiji.schema.layout.impl.cassandra.CassandraTableLayoutDatabase

/**
* (c) Copyright 2012 WibiData, Inc.
*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* 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 org.kiji.schema.layout.impl.cassandra;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.NavigableMap;
import java.util.Set;

import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.hadoop.hbase.HConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.kiji.annotations.ApiAudience;
import org.kiji.commons.ByteUtils;
import org.kiji.schema.KijiCellDecoder;
import org.kiji.schema.KijiCellEncoder;
import org.kiji.schema.KijiSchemaTable;
import org.kiji.schema.KijiTableNotFoundException;
import org.kiji.schema.KijiURI;
import org.kiji.schema.SpecificCellDecoderFactory;
import org.kiji.schema.avro.CellSchema;
import org.kiji.schema.avro.SchemaStorage;
import org.kiji.schema.avro.SchemaType;
import org.kiji.schema.avro.TableLayoutBackupEntry;
import org.kiji.schema.avro.TableLayoutDesc;
import org.kiji.schema.avro.TableLayoutsBackup;
import org.kiji.schema.cassandra.CassandraTableName;
import org.kiji.schema.impl.AvroCellEncoder;
import org.kiji.schema.impl.cassandra.CassandraAdmin;
import org.kiji.schema.layout.CellSpec;
import org.kiji.schema.layout.InvalidLayoutException;
import org.kiji.schema.layout.KijiTableLayout;
import org.kiji.schema.layout.KijiTableLayoutDatabase;
import org.kiji.schema.layout.TableLayoutBuilder;
import org.kiji.schema.layout.TableLayoutBuilder.LayoutOptions;
import org.kiji.schema.layout.TableLayoutBuilder.LayoutOptions.SchemaFormat;

/**
* <p>Manages Kiji table layouts using a Cassandra table as a backing store.</p>
*
* <p>
* The C* primary key is the name of the table, and the row has 3 columns:
*   <li> timestamp (needs to be explicit in C*);</li>
*   <li> the layout update, as specified by the user/submitter; </li>
*   <li> the effective layout after applying the update; </li>
*   <li> a hash of the effective layout. </li>
* </p>
*
* <p>
* Layouts and layout updates are encoded as Kiji cells, using Avro schema hashes, and as
* TableLayoutDesc Avro records.
* </p>
*
* <p>A static method, <code>getHColumnDescriptor</code> returns the description of an
* HColumn that should be used to construct the HTable for the backing store.</p>
*/
@ApiAudience.Private
public final class CassandraTableLayoutDatabase implements KijiTableLayoutDatabase {
  private static final Logger LOG = LoggerFactory.getLogger(CassandraTableLayoutDatabase.class);

  public static final String QUALIFIER_TABLE = "table_name";
  public static final String QUALIFIER_TIME = "time";

  /**
   * C* column used to store layout updates.
   * Layout updates are binary encoded TableLayoutDesc records.
   */
  public static final String QUALIFIER_UPDATE = "layout_update";

  /**
   * C* column used to store absolute layouts.
   * Table layouts are binary encoded TableLayoutDesc records.
   */
  public static final String QUALIFIER_LAYOUT = "layout";

  /**
   * C* column used to store layout IDs.
   * Currently, IDs are assigned using a long counter starting at 1, and encoded as a string.
   */
  public static final String QUALIFIER_LAYOUT_ID = "layout_id";

  /** URI of the Kiji instance this layout database is for. */
  private final KijiURI mKijiURI;

  /** The C* table to use to store the layouts. */
  private final CassandraTableName mMetaTableName;

  /** Cassandra administration object used for sending CQL requests to C* cluster. */
  private final CassandraAdmin mAdmin;

  /** The schema table. */
  private final KijiSchemaTable mSchemaTable;

  /** Kiji cell encoder. */
  private final KijiCellEncoder mCellEncoder;

  /** Decoder for concrete layout cells. */
  private final KijiCellDecoder<TableLayoutDesc> mCellDecoder;

  private final PreparedStatement mGetRowsStatement;
  private final PreparedStatement mUpdateTableLayoutStatement;
  private final PreparedStatement mRemoveAllTableLayoutVersionsStatement;
  private final PreparedStatement mRemoveRecentTableLayoutVersionsStatement;
  private final PreparedStatement mListTablesStatement;

  private static final CellSchema CELL_SCHEMA = CellSchema.newBuilder()
      .setStorage(SchemaStorage.HASH)
      .setType(SchemaType.CLASS)
      .setValue(TableLayoutDesc.SCHEMA$.getFullName())
      .build();

  /**
   * Prepare statement to reuse many times.
   *
   * TODO (SCHEMA-748): Don't need statement preparation after adding statement cache.
   *
   * @return the prepared statement.
   */
  private PreparedStatement getGetRowsStatement() {
    final String queryText =
        String.format("SELECT * FROM %s WHERE %s=? LIMIT 1", mMetaTableName, QUALIFIER_TABLE);
    return mAdmin.getPreparedStatement(queryText);
  }

  /**
   * Prepare statement to reuse many times.
   *
   * TODO (SCHEMA-748): Don't need statement preparation after adding statement cache.
   *
   * @return the prepared statement.
   */
  private PreparedStatement getUpdateTableLayoutStatement() {
    final String queryText = String.format(
        "INSERT INTO %s (%s, %s, %s, %s, %s) VALUES (?, ?, ?, ?, ?)",
        mMetaTableName,
        QUALIFIER_TABLE,
        QUALIFIER_TIME,
        QUALIFIER_LAYOUT_ID,
        QUALIFIER_LAYOUT,
        QUALIFIER_UPDATE);
    return mAdmin.getPreparedStatement(queryText);
  }

  /**
   * Prepare statement to reuse many times.
   *
   * TODO (SCHEMA-748): Don't need statement preparation after adding statement cache.
   *
   * @return the prepared statement.
   */
  private PreparedStatement getRemoveAllTableLayoutVersionsStatement() {
    final String queryText =
        String.format("DELETE FROM %s WHERE %s=?", mMetaTableName, QUALIFIER_TABLE);
    return mAdmin.getPreparedStatement(queryText);
  }

  /**
   * Prepare statement to reuse many times.
   *
   * TODO (SCHEMA-748): Don't need statement preparation after adding statement cache.
   *
   * @return the prepared statement.
   */
  private PreparedStatement getRemoveRecentTableLayoutVersionsStatement() {
    final String queryText = String.format(
        "DELETE FROM %s WHERE %s=? AND %s=?",
        mMetaTableName,
        QUALIFIER_TABLE,
        QUALIFIER_TIME);
    return mAdmin.getPreparedStatement(queryText);
  }

  /**
   * Prepare statement to reuse many times.
   *
   * TODO (SCHEMA-748): Don't need statement preparation after adding statement cache.
   *
   * @return the prepared statement.
   */
  private PreparedStatement getListTablesStatement() {
    String queryText = String.format("SELECT %s FROM %s", QUALIFIER_TABLE, mMetaTableName);
    return mAdmin.getPreparedStatement(queryText);
  }

  /**
   * Install a table for storing table layout information.
   * @param admin A wrapper around an open C* session.
   * @param uri The KijiURI of the instance for this table.
   */
  public static void install(CassandraAdmin admin, KijiURI uri) {
    CassandraTableName tableName = CassandraTableName.getMetaLayoutTableName(uri);

    // Standard C* table layout.  Use text key + timestamp as composite primary key to allow
    // selection by timestamp.

    // I did not use timeuuid here because we need to be able to write timestamps sometimes.

    // For the rest of the table, the layout ID is a string, then we store the actual layout and
    // update as blobs.
    final String tableDescription = String.format(
        "CREATE TABLE %s (%s text, %s timestamp, %s text, %s blob, %s blob, PRIMARY KEY (%s, %s)) "
            + "WITH CLUSTERING ORDER BY (%s DESC);",
        tableName,
        QUALIFIER_TABLE,
        QUALIFIER_TIME,
        QUALIFIER_LAYOUT_ID,
        QUALIFIER_LAYOUT,
        QUALIFIER_UPDATE,
        QUALIFIER_TABLE,
        QUALIFIER_TIME,
        QUALIFIER_TIME);
    admin.createTable(tableName, tableDescription);
  }

  /**
   * Creates a new <code>CassandraTableLayoutDatabase</code> instance.
   *
   * <p>This class does not take ownership of the table.  The caller should close it when
   * it is no longer needed.</p>
   *
   * @param kijiURI URI of the Kiji instance this layout database belongs to.
   * @param admin The Cassandra cluster connection.
   * @param schemaTable The Kiji schema table.
   * @throws java.io.IOException on I/O error.
   */
  public CassandraTableLayoutDatabase(
      KijiURI kijiURI,
      CassandraAdmin admin,
      KijiSchemaTable schemaTable)
      throws IOException {
    mKijiURI = kijiURI;
    mMetaTableName = CassandraTableName.getMetaLayoutTableName(kijiURI);
    mAdmin = admin;
    mSchemaTable = Preconditions.checkNotNull(schemaTable);
    final CellSpec cellSpec = CellSpec.fromCellSchema(CELL_SCHEMA, mSchemaTable);
    mCellEncoder = new AvroCellEncoder(cellSpec);
    mCellDecoder = SpecificCellDecoderFactory.get().create(cellSpec);
    mGetRowsStatement = getGetRowsStatement();
    mRemoveAllTableLayoutVersionsStatement = getRemoveAllTableLayoutVersionsStatement();
    mUpdateTableLayoutStatement = getUpdateTableLayoutStatement();
    mRemoveRecentTableLayoutVersionsStatement = getRemoveRecentTableLayoutVersionsStatement();
    mListTablesStatement = getListTablesStatement();
  }

  /** {@inheritDoc} */
  @Override
  public KijiTableLayout updateTableLayout(String tableName, TableLayoutDesc layoutUpdate)
      throws IOException {

    // Normalize the new layout to use schema UIDs:
    final TableLayoutBuilder layoutBuilder = new TableLayoutBuilder(mSchemaTable);
    final TableLayoutDesc update = layoutBuilder.normalizeTableLayoutDesc(
        layoutUpdate,
        new LayoutOptions()
            .setSchemaFormat(SchemaFormat.UID));

    // Fetch all the layout history:
    final List<KijiTableLayout> layouts =
        getTableLayoutVersions(tableName, HConstants.ALL_VERSIONS);
    final KijiTableLayout currentLayout = layouts.isEmpty() ? null : layouts.get(0);
    final KijiTableLayout tableLayout = KijiTableLayout.createUpdatedLayout(update, currentLayout);

    Preconditions.checkArgument(tableName.equals(tableLayout.getName()));

    // Set of all the former layout IDs:
    final Set<String> layoutIDs = Sets.newHashSet();
    for (KijiTableLayout layout : layouts) {
      layoutIDs.add(layout.getDesc().getLayoutId());
    }

    final String refLayoutIdStr = update.getReferenceLayout();

    final boolean hasCurrentLayout = (null != currentLayout);
    final boolean hasRefLayoutId = (null != refLayoutIdStr);
    if (hasCurrentLayout && !hasRefLayoutId) {
      throw new IOException(String.format(
          "Layout for table '%s' does not specify reference layout ID.", tableName));
    }
    if (!hasCurrentLayout && hasRefLayoutId) {
      throw new IOException(String.format(
          "Initial layout for table '%s' must not specify reference layout ID.", tableName));
    }

    final String layoutId = tableLayout.getDesc().getLayoutId();

    if (layoutIDs.contains(layoutId)) {
      throw new InvalidLayoutException(tableLayout,
          String.format("Layout ID '%s' already exists", layoutId));
    }

    //String metaTableName = mMetaTableName;

    Preconditions.checkNotNull(mUpdateTableLayoutStatement);
    // TODO: This should do a "check-and-put" to match the HBase implementation.
    mAdmin.execute(
        mUpdateTableLayoutStatement.bind(
            tableName,
            new Date(),
            layoutId,
            ByteBuffer.wrap(mCellEncoder.encode(tableLayout.getDesc())),
            ByteBuffer.wrap(mCellEncoder.encode(update)))
    );

    // Flush the writer schema for the Avro table layout first so other readers can see it.
    mSchemaTable.flush();

    return tableLayout;
  }

  /** {@inheritDoc} */
  @Override
  public KijiTableLayout getTableLayout(String table) throws IOException {
    final List<KijiTableLayout> layouts = getTableLayoutVersions(table, 1);
    if (layouts.isEmpty()) {
      throw new KijiTableNotFoundException(
          KijiURI.newBuilder(mKijiURI).withTableName(table).build());
    }
    return layouts.get(0);
  }

  /**
   * Internal helper method containing common code for ready values for a given key from the table.
   * @param table Name of the table for which to fetch the values (part of the key-value database
   *              key).
   * @param numVersions Number of versions to fetch for the given table, key combination.
   * @return A list of C* rows for the query.
   */
  private List<Row> getRows(String table, int numVersions) {
    Preconditions.checkArgument(numVersions >= 1"numVersions must be positive");
    Preconditions.checkNotNull(mGetRowsStatement);
    return mAdmin.execute(mGetRowsStatement.bind(table)).all();
  }

  /** {@inheritDoc} */
  @Override
  public List<KijiTableLayout> getTableLayoutVersions(String table, int numVersions)
      throws IOException {
    Preconditions.checkArgument(numVersions >= 1"numVersions must be positive");

    final List<Row> rows = getRows(table, numVersions);

    // Convert result into a list of bytes
    final List<KijiTableLayout> layouts = Lists.newArrayList();
    for (Row row: rows) {
      ByteBuffer blob = row.getBytes(QUALIFIER_LAYOUT);
      byte[] bytes = ByteUtils.toBytes(blob);
      layouts.add(KijiTableLayout.newLayout(decodeTableLayoutDesc(bytes)));
    }
    return layouts;
  }

  /** {@inheritDoc} */
  @Override
  public NavigableMap<Long, KijiTableLayout> getTimedTableLayoutVersions(
      String table,
      int numVersions
  ) throws IOException {
    Preconditions.checkArgument(numVersions >= 1, "numVersions must be positive");

    final List<Row> rows = getRows(table, numVersions);

    // Convert result into a map from timestamps to values
    final NavigableMap<Long, KijiTableLayout> timedValues = Maps.newTreeMap();
    for (Row row: rows) {
      ByteBuffer blob = row.getBytes(QUALIFIER_LAYOUT);
      byte[] bytes = ByteUtils.toBytes(blob);
      KijiTableLayout layout = KijiTableLayout.newLayout(decodeTableLayoutDesc(bytes));

      Long timestamp = row.getDate(QUALIFIER_TIME).getTime();
      Preconditions.checkState(timedValues.put(timestamp, layout) == null);
    }
    return timedValues;
  }

  /** {@inheritDoc} */
  @Override
  public void removeAllTableLayoutVersions(String table) throws IOException {
    // TODO: Check for success?
    Preconditions.checkNotNull(mRemoveAllTableLayoutVersionsStatement);
    mAdmin.execute(mRemoveAllTableLayoutVersionsStatement.bind(table));
  }

  /** {@inheritDoc} */
  @Override
  public void removeRecentTableLayoutVersions(String table, int numVersions) throws IOException {
    Preconditions.checkArgument(numVersions >= 1, "numVersions must be positive");
    // Unclear how to do this in C* without first reading about the most-recent versions

    // Get a list of versions to delete
    List<Row> rows = getRows(table, numVersions);

    Preconditions.checkNotNull(mRemoveRecentTableLayoutVersionsStatement);
    for (Row row: rows) {
      Long timestamp = row.getDate(QUALIFIER_TIME).getTime();
      mAdmin.execute(
          mRemoveRecentTableLayoutVersionsStatement.bind(table, new Date(timestamp)));
    }
  }

  /** {@inheritDoc} */
  @Override
  public List<String> listTables() throws IOException {
    Preconditions.checkNotNull(mListTablesStatement);


    // Just return a set of in-use tables
    ResultSet resultSet = mAdmin.execute(mListTablesStatement.bind());
    Set<String> keys = new HashSet<String>();

    // This code makes me miss Scala
    for (Row row: resultSet.all()) {
      keys.add(row.getString(QUALIFIER_TABLE));
    }

    List<String> list = new ArrayList<String>();
    list.addAll(keys);
    return list;
  }

  /** {@inheritDoc} */
  @Override
  public boolean tableExists(String tableName) throws IOException {
    List<String> tables = listTables();
    return tables.contains(tableName);
  }

  /** {@inheritDoc} */
  @Override
  public TableLayoutsBackup layoutsToBackup(String table) throws IOException {
    final List<TableLayoutBackupEntry> history = Lists.newArrayList();
    final TableLayoutsBackup backup = TableLayoutsBackup.newBuilder().setLayouts(history).build();

    final List<Row> rows = getRows(table, Integer.MAX_VALUE);
    if (rows.isEmpty()) {
      LOG.info(String.format(
          "There is no row in the MetaTable named '%s' or the row is empty.", table));
      return backup;
    }

    for (Row row: rows) {
      final long timestamp = row.getDate(QUALIFIER_TIME).getTime();
      final TableLayoutDesc layout =
          decodeTableLayoutDesc(ByteUtils.toBytes(row.getBytes(QUALIFIER_LAYOUT)));

      // TODO: May need some check here that the update is not null
      final TableLayoutDesc update =
          decodeTableLayoutDesc(ByteUtils.toBytes(row.getBytes(QUALIFIER_UPDATE)));

      history.add(TableLayoutBackupEntry.newBuilder()
          .setLayout(layout)
          .setUpdate(update)
          .setTimestamp(timestamp)
          .build());
    }
    return backup;
  }

  /** {@inheritDoc} */
  @Override
  public void restoreLayoutsFromBackup(String tableName, TableLayoutsBackup layoutBackup)
      throws IOException {
    LOG.info(String.format("Restoring layout history for table '%s'.", tableName));

    // Looks like we need insertions with and without updates and timestamps.

    // TODO: Make this query a member of the class and prepare in the constructor
    String queryTextInsertAll = String.format(
        "INSERT INTO %s (%s, %s, %s, %s) VALUES (?, ?, ?, ?)",
        mMetaTableName,
        QUALIFIER_TABLE,
        QUALIFIER_TIME,
        QUALIFIER_LAYOUT,
        QUALIFIER_UPDATE);
    PreparedStatement preparedStatementInsertAll = mAdmin.getPreparedStatement(queryTextInsertAll);

    String queryTextInsertLayout = String.format(
        "INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)",
        mMetaTableName,
        QUALIFIER_TABLE,
        QUALIFIER_TIME,
        QUALIFIER_LAYOUT);
    final PreparedStatement insertLayoutStatement =
        mAdmin.getPreparedStatement(queryTextInsertLayout);

    // TODO: Unclear what happens to layout IDs here...
    for (TableLayoutBackupEntry lbe : layoutBackup.getLayouts()) {
      final byte[] layoutBytes = encodeTableLayoutDesc(lbe.getLayout());
      final ByteBuffer layoutByteBuffer = ByteBuffer.wrap(layoutBytes);

      if (lbe.getUpdate() != null) {
        final byte[] updateBytes = encodeTableLayoutDesc(lbe.getUpdate());
        final ByteBuffer updateByteBuffer = ByteBuffer.wrap(updateBytes);
        final long timestamp = lbe.getTimestamp();

        mAdmin.execute(preparedStatementInsertAll.bind(
            tableName,
            new Date(timestamp),
            layoutByteBuffer,
            updateByteBuffer));
      } else {
        mAdmin.execute(insertLayoutStatement.bind(tableName, new Date(), layoutByteBuffer));
      }
    }
    // TODO: Some kind of flush?
  }

  /**
   * Decodes a table layout descriptor from binary.
   *
   * @param bytes Serialized table layout descriptor.
   * @return Deserialized table layout descriptor.
   * @throws java.io.IOException on I/O or decoding error.
   */
  private TableLayoutDesc decodeTableLayoutDesc(byte[] bytes) throws IOException {
    return mCellDecoder.decodeValue(bytes);
  }

  /**
   * Encodes a table layout descriptor to binary.
   *
   * @param desc Table layout descriptor to serialize.
   * @return Table layout descriptor encoded as bytes.
   * @throws java.io.IOException on I/O or encoding error.
   */
  private byte[] encodeTableLayoutDesc(TableLayoutDesc desc) throws IOException {
    return mCellEncoder.encode(desc);
  }

  /** {@inheritDoc} */
  @Override
  public String toString() {
    return Objects.toStringHelper(CassandraTableLayoutDatabase.class)
        .add("uri", mKijiURI)
        .toString();
  }
}
TOP

Related Classes of org.kiji.schema.layout.impl.cassandra.CassandraTableLayoutDatabase

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.