/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.omid.transaction; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.CellUtil; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.util.Bytes; import org.apache.omid.HBaseShims; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Charsets; import com.google.common.base.Objects; import com.google.common.base.Objects.ToStringHelper; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; @SuppressWarnings("all") public final class CellUtils { private static final Logger LOG = LoggerFactory.getLogger(CellUtils.class); static final byte[] SHADOW_CELL_SUFFIX = "\u0080".getBytes(Charsets.UTF_8); // Non printable char (128 ASCII) //Prefix starts with 0 to apear before other cells in TransactionVisibilityFilter static final byte[] SHADOW_CELL_PREFIX = "\u0000\u0080".getBytes(Charsets.UTF_8); static byte[] DELETE_TOMBSTONE = HConstants.EMPTY_BYTE_ARRAY; static byte[] LEGACY_DELETE_TOMBSTONE = Bytes.toBytes("__OMID_TOMBSTONE__"); public static final byte[] FAMILY_DELETE_QUALIFIER = HConstants.EMPTY_BYTE_ARRAY; public static final String TRANSACTION_ATTRIBUTE = "__OMID_TRANSACTION__"; /**/ public static final String CLIENT_GET_ATTRIBUTE = "__OMID_CLIENT_GET__"; public static final String LL_ATTRIBUTE = "__OMID_LL__"; /** * Utility interface to get rid of the dependency on HBase server package */ interface CellGetter { Result get(Get get) throws IOException; } /** * Returns true if the particular cell passed exists in the datastore. * @param row row * @param family column family * @param qualifier columnn name * @param version version * @param cellGetter an instance of CellGetter * @return true if the cell specified exists. false otherwise * @throws IOException */ public static boolean hasCell(byte[] row, byte[] family, byte[] qualifier, long version, CellGetter cellGetter) throws IOException { Get get = new Get(row); get.addColumn(family, qualifier); get.setTimeStamp(version); Result result = cellGetter.get(get); return result.containsColumn(family, qualifier); } /** * Returns true if the particular cell passed has a corresponding shadow cell in the datastore * @param row row * @param family column family * @param qualifier columnn name * @param version version * @param cellGetter an instance of CellGetter * @return true if it has a shadow cell. false otherwise. * @throws IOException */ public static boolean hasShadowCell(byte[] row, byte[] family, byte[] qualifier, long version, CellGetter cellGetter) throws IOException { return hasCell(row, family, addShadowCellSuffixPrefix(qualifier), version, cellGetter); } /** * Builds a new qualifier composed of the HBase qualifier passed + the shadow cell suffix. * @param qualifierArray the qualifier to be suffixed * @param qualOffset the offset where the qualifier starts * @param qualLength the qualifier length * @return the suffixed qualifier */ public static byte[] addShadowCellSuffixPrefix(byte[] qualifierArray, int qualOffset, int qualLength) { byte[] result = new byte[qualLength + SHADOW_CELL_SUFFIX.length + SHADOW_CELL_PREFIX.length]; System.arraycopy(SHADOW_CELL_PREFIX, 0, result,0 , SHADOW_CELL_PREFIX.length); System.arraycopy(qualifierArray, qualOffset, result, SHADOW_CELL_PREFIX.length, qualLength); System.arraycopy(SHADOW_CELL_SUFFIX, 0, result, qualLength + SHADOW_CELL_PREFIX.length, SHADOW_CELL_SUFFIX.length); return result; } /** * Builds a new qualifier composed of the HBase qualifier passed + the shadow cell suffix. * Contains a reduced signature to avoid boilerplate code in client side. * @param qualifier * the qualifier to be suffixed * @return the suffixed qualifier */ public static byte[] addShadowCellSuffixPrefix(byte[] qualifier) { return addShadowCellSuffixPrefix(qualifier, 0, qualifier.length); } /** * Builds a new qualifier removing the shadow cell suffix from the * passed HBase qualifier. * @param qualifier the qualifier to remove the suffix from * @param qualOffset the offset where the qualifier starts * @param qualLength the qualifier length * @return the new qualifier without the suffix */ public static byte[] removeShadowCellSuffixPrefix(byte[] qualifier, int qualOffset, int qualLength) { if (endsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_SUFFIX)) { if (startsWith(qualifier, qualOffset,qualLength, SHADOW_CELL_PREFIX)) { return Arrays.copyOfRange(qualifier, qualOffset + SHADOW_CELL_PREFIX.length, qualOffset + (qualLength - SHADOW_CELL_SUFFIX.length)); } else { //support backward competatbiliy return Arrays.copyOfRange(qualifier, qualOffset,qualOffset + (qualLength - SHADOW_CELL_SUFFIX.length)); } } throw new IllegalArgumentException( "Can't find shadow cell suffix in qualifier " + Bytes.toString(qualifier)); } /** * Returns the qualifier length removing the shadow cell suffix and prefix. In case that que suffix is not found, * just returns the length of the qualifier passed. * @param qualifier the qualifier to remove the suffix from * @param qualOffset the offset where the qualifier starts * @param qualLength the qualifier length * @return the qualifier length without the suffix */ public static int qualifierLengthFromShadowCellQualifier(byte[] qualifier, int qualOffset, int qualLength) { if (endsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_SUFFIX)) { if (startsWith(qualifier,qualOffset, qualLength, SHADOW_CELL_PREFIX)) { return qualLength - SHADOW_CELL_SUFFIX.length - SHADOW_CELL_PREFIX.length; } else { return qualLength - SHADOW_CELL_SUFFIX.length; } } return qualLength; } /** * Returns the qualifier length removing the shadow cell suffix and prefix. In case that que suffix is not found, * just returns the length of the qualifier passed. * @param qualifier the qualifier to remove the suffix from * @param qualOffset the offset where the qualifier starts * @param qualLength the qualifier length * @return the qualifier length without the suffix */ public static int qualifierOffsetFromShadowCellQualifier(byte[] qualifier, int qualOffset, int qualLength) { if (startsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_PREFIX)) { return qualOffset + SHADOW_CELL_PREFIX.length; } return qualOffset; } /** * Complement to matchingQualifier() methods in HBase's CellUtil.class * @param left the cell to compare the qualifier * @param qualArray the explicit qualifier array passed * @param qualOffset the explicit qualifier offset passed * @param qualLen the explicit qualifier length passed * @return whether the qualifiers are equal or not */ public static boolean matchingQualifier(final Cell left, final byte[] qualArray, int qualOffset, int qualLen) { return Bytes.equals(left.getQualifierArray(), left.getQualifierOffset(), left.getQualifierLength(), qualArray, qualOffset, qualLen); } /** * Check that the cell passed meets the requirements for a valid cell identifier with Omid. Basically, users can't: * 1) specify a timestamp * 2) use a particular suffix in the qualifier */ public static void validateCell(Cell cell, long startTimestamp) { // Throw exception if timestamp is set by the user if (cell.getTimestamp() != HConstants.LATEST_TIMESTAMP && cell.getTimestamp() != startTimestamp) { throw new IllegalArgumentException( "Timestamp not allowed in transactional user operations"); } // Throw exception if using a non-allowed qualifier if (isShadowCell(cell)) { throw new IllegalArgumentException( "Reserved string used in column qualifier"); } } /** * Returns whether a cell contains a qualifier that is a delete cell * column qualifier or not. * @param cell the cell to check if contains the delete cell qualifier * @return whether the cell passed contains a delete cell qualifier or not */ public static boolean isFamilyDeleteCell(Cell cell) { return CellUtil.matchingQualifier(cell, CellUtils.FAMILY_DELETE_QUALIFIER) && CellUtil.matchingValue(cell, HConstants.EMPTY_BYTE_ARRAY); } /** * Returns whether a cell contains a qualifier that is a shadow cell * column qualifier or not. * @param cell the cell to check if contains the shadow cell qualifier * @return whether the cell passed contains a shadow cell qualifier or not */ public static boolean isShadowCell(Cell cell) { byte[] qualifier = cell.getQualifierArray(); int qualOffset = cell.getQualifierOffset(); int qualLength = cell.getQualifierLength(); return endsWith(qualifier, qualOffset, qualLength, SHADOW_CELL_SUFFIX); } private static boolean endsWith(byte[] value, int offset, int length, byte[] suffix) { if (length <= suffix.length) { return false; } int suffixOffset = offset + length - suffix.length; int result = Bytes.compareTo(value, suffixOffset, suffix.length, suffix, 0, suffix.length); return result == 0; } private static boolean startsWith(byte[] value, int offset, int length, byte[] prefix) { if (length <= prefix.length) { return false; } int result = Bytes.compareTo(value, offset, prefix.length, prefix, 0, prefix.length); return result == 0; } /** * Returns if a cell is marked as a tombstone. * @param cell the cell to check * @return whether the cell is marked as a tombstone or not */ public static boolean isTombstone(Cell cell) { return CellUtil.matchingValue(cell, DELETE_TOMBSTONE) || CellUtil.matchingValue(cell, LEGACY_DELETE_TOMBSTONE); } /** * Returns a new shadow cell created from a particular cell. * @param cell * the cell to reconstruct the shadow cell from. * @param shadowCellValue * the value for the new shadow cell created * @return the brand-new shadow cell */ public static Cell buildShadowCellFromCell(Cell cell, byte[] shadowCellValue) { byte[] shadowCellQualifier = addShadowCellSuffixPrefix(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()); return new KeyValue( cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength(), shadowCellQualifier, 0, shadowCellQualifier.length, cell.getTimestamp(), KeyValue.Type.codeToType(cell.getTypeByte()), shadowCellValue, 0, shadowCellValue.length); } /** * Analyzes a list of cells, associating the corresponding shadow cell if present. * * @param cells the list of cells to classify * @return a sorted map associating each cell with its shadow cell */ public static SortedMap<Cell, Optional<Cell>> mapCellsToShadowCells(List<Cell> cells) { // Move CellComparator to HBaseSims for 2.0 support // Need to access through CellComparatorImpl.COMPARATOR SortedMap<Cell, Optional<Cell>> cellToShadowCellMap = new TreeMap<Cell, Optional<Cell>>(HBaseShims.cellComparatorInstance()); Map<CellId, Cell> cellIdToCellMap = new HashMap<CellId, Cell>(); Map<CellId, Cell> cellIdToSCCellMap = new HashMap<CellId, Cell>(); for (Cell cell : cells) { if (!isShadowCell(cell)) { CellId key = new CellId(cell, false); // Get the current cell and compare the values Cell storedCell = cellIdToCellMap.get(key); if (storedCell != null) { if (CellUtil.matchingValue(cell, storedCell)) { // TODO: Should we check also here the MVCC and swap if its greater??? // Values are the same, ignore } else { if (cell.getSequenceId() > storedCell.getSequenceId()) { // Swap values Optional<Cell> previousValue = cellToShadowCellMap.remove(storedCell); Preconditions.checkNotNull(previousValue, "Should contain an Optional<Cell> value"); cellIdToCellMap.put(key, cell); cellToShadowCellMap.put(cell, previousValue); } else { LOG.warn("Cell {} with an earlier MVCC found. Ignoring...", cell); } } } else { cellIdToCellMap.put(key, cell); Cell sc = cellIdToSCCellMap.get(key); if (sc != null) { cellToShadowCellMap.put(cell, Optional.of(sc)); } else { cellToShadowCellMap.put(cell, Optional.<Cell>absent()); } } } else { CellId key = new CellId(cell, true); Cell savedCell = cellIdToCellMap.get(key); if (savedCell != null) { Cell originalCell = savedCell; cellToShadowCellMap.put(originalCell, Optional.of(cell)); } else { cellIdToSCCellMap.put(key, cell); } } } return cellToShadowCellMap; } private static class CellId { private static final int MIN_BITS = 32; private final Cell cell; private final boolean isShadowCell; CellId(Cell cell, boolean isShadowCell) { this.cell = cell; this.isShadowCell = isShadowCell; } Cell getCell() { return cell; } boolean isShadowCell() { return isShadowCell; } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof CellId)) return false; CellId otherCellId = (CellId) o; Cell otherCell = otherCellId.getCell(); // Row comparison if (!CellUtil.matchingRow(otherCell, cell)) { return false; } // Family comparison if (!CellUtil.matchingFamily(otherCell, cell)) { return false; } // Qualifier comparison int qualifierLength = cell.getQualifierLength(); int qualifierOffset = cell.getQualifierOffset(); int otherQualifierLength = otherCell.getQualifierLength(); int otherQualifierOffset = otherCell.getQualifierOffset(); if (isShadowCell()) { qualifierLength = qualifierLengthFromShadowCellQualifier(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()); qualifierOffset = qualifierOffsetFromShadowCellQualifier(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()); } if (otherCellId.isShadowCell()) { otherQualifierLength = qualifierLengthFromShadowCellQualifier(otherCell.getQualifierArray(), otherCell.getQualifierOffset(), otherCell.getQualifierLength()); otherQualifierOffset = qualifierOffsetFromShadowCellQualifier(otherCell.getQualifierArray(), otherCell.getQualifierOffset(), otherCell.getQualifierLength()); } if (!Bytes.equals(cell.getQualifierArray(), qualifierOffset, qualifierLength, otherCell.getQualifierArray(), otherQualifierOffset, otherQualifierLength)) { return false; } // Timestamp comparison return otherCell.getTimestamp() == cell.getTimestamp(); } @Override public int hashCode() { Hasher hasher = Hashing.goodFastHash(MIN_BITS).newHasher(); hasher.putBytes(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength()); hasher.putBytes(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength()); int qualifierLength = cell.getQualifierLength(); int qualifierOffset = cell.getQualifierOffset(); if (isShadowCell()) { qualifierLength = qualifierLengthFromShadowCellQualifier(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()); if (startsWith(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength(), SHADOW_CELL_PREFIX)) { qualifierOffset = qualifierOffset + SHADOW_CELL_PREFIX.length; } } hasher.putBytes(cell.getQualifierArray(),qualifierOffset , qualifierLength); hasher.putLong(cell.getTimestamp()); return hasher.hash().asInt(); } @Override public String toString() { ToStringHelper helper = Objects.toStringHelper(this); helper.add("row", Bytes.toStringBinary(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength())); helper.add("family", Bytes.toString(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength())); helper.add("is shadow cell?", isShadowCell); helper.add("qualifier", Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength())); if (isShadowCell()) { int qualifierLength = qualifierLengthFromShadowCellQualifier(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()); byte[] cellWithoutSc = removeShadowCellSuffixPrefix(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()); helper.add("qualifier whithout shadow cell suffix", Bytes.toString(cellWithoutSc)); } helper.add("ts", cell.getTimestamp()); return helper.toString(); } } }