/* * Copyright © 2015 Cask Data, Inc. * * 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 co.cask.cdap.data.tools; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.api.dataset.DatasetSpecification; import co.cask.cdap.api.dataset.table.Table; import co.cask.cdap.data2.datafabric.dataset.DatasetMetaTableUtil; import co.cask.cdap.data2.util.TableId; import co.cask.cdap.data2.util.hbase.HBaseTableUtil; import co.cask.cdap.data2.util.hbase.ScanBuilder; import co.cask.cdap.proto.Id; import com.google.common.annotations.VisibleForTesting; import com.google.gson.Gson; import com.google.inject.Inject; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.client.HBaseAdmin; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.ResultScanner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * Upgrading from CDAP version < 3.3 to CDAP version 3.3. * This requires updating the TTL property for DatasetSpecification in the DatasetInstanceMDS table. */ public class DatasetSpecificationUpgrader { private static final Logger LOG = LoggerFactory.getLogger(DatasetSpecificationUpgrader.class); private static final Gson GSON = new Gson(); private static final String TTL_UPDATED = "table.ttl.migrated.to.seconds"; private final HBaseTableUtil tableUtil; private final Configuration conf; @Inject public DatasetSpecificationUpgrader(HBaseTableUtil tableUtil, Configuration conf) { this.tableUtil = tableUtil; this.conf = conf; } /** * Updates the TTL in the {@link co.cask.cdap.data2.datafabric.dataset.service.mds.DatasetInstanceMDS} * table for CDAP versions prior to 3.3. * <p> * The TTL for {@link DatasetSpecification} was stored in milliseconds. * Since the spec (as of CDAP version 3.3) is in seconds, the instance MDS entries must be updated. * This is to be called only if the current CDAP version is < 3.3. * </p> * @throws Exception */ public void upgrade() throws Exception { TableId datasetSpecId = TableId.from(Id.Namespace.SYSTEM.getId(), DatasetMetaTableUtil.INSTANCE_TABLE_NAME); HBaseAdmin hBaseAdmin = new HBaseAdmin(conf); if (!tableUtil.tableExists(hBaseAdmin, datasetSpecId)) { LOG.error("Dataset instance table does not exist: {}. Should not happen", datasetSpecId); return; } HTable specTable = tableUtil.createHTable(conf, datasetSpecId); try { ScanBuilder scanBuilder = tableUtil.buildScan(); scanBuilder.setTimeRange(0, HConstants.LATEST_TIMESTAMP); scanBuilder.setMaxVersions(); try (ResultScanner resultScanner = specTable.getScanner(scanBuilder.build())) { Result result; while ((result = resultScanner.next()) != null) { Put put = new Put(result.getRow()); for (Map.Entry<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>> familyMap : result.getMap().entrySet()) { for (Map.Entry<byte[], NavigableMap<Long, byte[]>> columnMap : familyMap.getValue().entrySet()) { for (Map.Entry<Long, byte[]> columnEntry : columnMap.getValue().entrySet()) { Long timeStamp = columnEntry.getKey(); byte[] colVal = columnEntry.getValue(); // a deleted dataset can still show up here since BufferingTable doesn't actually delete, but // writes a null value. The fact that we need to know that implementation detail here is bad. // If we could use Table here instead of HTable, this would be hidden from us. if (colVal == null || colVal.length == 0) { continue; } String specEntry = Bytes.toString(colVal); DatasetSpecification specification = GSON.fromJson(specEntry, DatasetSpecification.class); DatasetSpecification updatedSpec = updateTTLInSpecification(specification, null); colVal = Bytes.toBytes(GSON.toJson(updatedSpec)); put.add(familyMap.getKey(), columnMap.getKey(), timeStamp, colVal); } } } // might not need to put anything if all columns were skipped because they are delete markers. if (put.size() > 0) { specTable.put(put); } } } } finally { specTable.flushCommits(); specTable.close(); } } private Map<String, String> updatedProperties(Map<String, String> properties) { if (properties.containsKey(Table.PROPERTY_TTL) && !properties.containsKey(TTL_UPDATED)) { SortedMap<String, String> updatedProperties = new TreeMap<>(properties); long updatedValue = TimeUnit.MILLISECONDS.toSeconds(Long.valueOf(updatedProperties.get(Table.PROPERTY_TTL))); updatedProperties.put(Table.PROPERTY_TTL, String.valueOf(updatedValue)); updatedProperties.put(TTL_UPDATED, "true"); return updatedProperties; } return properties; } @VisibleForTesting DatasetSpecification updateTTLInSpecification(DatasetSpecification specification, @Nullable String parentName) { Map<String, String> properties = updatedProperties(specification.getProperties()); List<DatasetSpecification> updatedSpecs = new ArrayList<>(); for (DatasetSpecification datasetSpecification : specification.getSpecifications().values()) { updatedSpecs.add(updateTTLInSpecification(datasetSpecification, specification.getName())); } String specName = specification.getName(); if (parentName != null && specification.getName().startsWith(parentName)) { specName = specification.getName().substring(parentName.length() + 1); } return DatasetSpecification.builder(specName, specification.getType()).properties(properties).datasets(updatedSpecs).build(); } }