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.master.procedure; 019 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.List; 024import org.apache.hadoop.hbase.HBaseIOException; 025import org.apache.hadoop.hbase.TableName; 026import org.apache.hadoop.hbase.TableNotDisabledException; 027import org.apache.hadoop.hbase.TableNotFoundException; 028import org.apache.hadoop.hbase.client.RegionInfo; 029import org.apache.hadoop.hbase.client.RegionInfoBuilder; 030import org.apache.hadoop.hbase.client.RegionReplicaUtil; 031import org.apache.hadoop.hbase.client.TableDescriptor; 032import org.apache.hadoop.hbase.master.MasterCoprocessorHost; 033import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer; 034import org.apache.hadoop.hbase.util.ModifyRegionUtils; 035import org.apache.yetus.audience.InterfaceAudience; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; 040import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos; 041import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos; 042import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.TruncateTableState; 043 044@InterfaceAudience.Private 045public class TruncateTableProcedure extends AbstractStateMachineTableProcedure<TruncateTableState> { 046 private static final Logger LOG = LoggerFactory.getLogger(TruncateTableProcedure.class); 047 048 private boolean preserveSplits; 049 private List<RegionInfo> regions; 050 private TableDescriptor tableDescriptor; 051 private TableName tableName; 052 private String recoverySnapshotName; 053 054 public TruncateTableProcedure() { 055 // Required by the Procedure framework to create the procedure on replay 056 super(); 057 } 058 059 public TruncateTableProcedure(final MasterProcedureEnv env, final TableName tableName, 060 boolean preserveSplits) throws HBaseIOException { 061 this(env, tableName, preserveSplits, null); 062 } 063 064 public TruncateTableProcedure(final MasterProcedureEnv env, final TableName tableName, 065 boolean preserveSplits, ProcedurePrepareLatch latch) throws HBaseIOException { 066 super(env, latch); 067 this.tableName = tableName; 068 preflightChecks(env, false); 069 this.preserveSplits = preserveSplits; 070 } 071 072 @Override 073 protected Flow executeFromState(final MasterProcedureEnv env, TruncateTableState state) 074 throws InterruptedException { 075 if (LOG.isTraceEnabled()) { 076 LOG.trace(this + " execute state=" + state); 077 } 078 try { 079 switch (state) { 080 case TRUNCATE_TABLE_PRE_OPERATION: 081 // Verify if we can truncate the table 082 if (!prepareTruncate(env)) { 083 assert isFailed() : "the truncate should have an exception here"; 084 return Flow.NO_MORE_STATE; 085 } 086 087 // TODO: Move out... in the acquireLock() 088 LOG.debug("waiting for '" + getTableName() + "' regions in transition"); 089 regions = env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()); 090 RegionReplicaUtil.removeNonDefaultRegions(regions); 091 assert regions != null && !regions.isEmpty() : "unexpected 0 regions"; 092 ProcedureSyncWait.waitRegionInTransition(env, regions); 093 094 // Call coprocessors 095 preTruncate(env); 096 097 // We need to cache table descriptor in the initial stage, so that it's saved within 098 // the procedure stage and can get recovered if the procedure crashes between 099 // TRUNCATE_TABLE_REMOVE_FROM_META and TRUNCATE_TABLE_CREATE_FS_LAYOUT 100 tableDescriptor = env.getMasterServices().getTableDescriptors().get(tableName); 101 102 // Check if we should create a recovery snapshot 103 if (RecoverySnapshotUtils.isRecoveryEnabled(env)) { 104 setNextState(TruncateTableState.TRUNCATE_TABLE_SNAPSHOT); 105 } else { 106 setNextState(TruncateTableState.TRUNCATE_TABLE_CLEAR_FS_LAYOUT); 107 } 108 break; 109 case TRUNCATE_TABLE_SNAPSHOT: 110 // Create recovery snapshot procedure as child procedure 111 recoverySnapshotName = RecoverySnapshotUtils.generateSnapshotName(tableName); 112 SnapshotProcedure snapshotProcedure = RecoverySnapshotUtils.createSnapshotProcedure(env, 113 tableName, recoverySnapshotName, tableDescriptor); 114 // Submit snapshot procedure as child procedure 115 addChildProcedure(snapshotProcedure); 116 LOG.debug("Creating recovery snapshot {} for table {} before truncation", 117 recoverySnapshotName, tableName); 118 setNextState(TruncateTableState.TRUNCATE_TABLE_CLEAR_FS_LAYOUT); 119 break; 120 case TRUNCATE_TABLE_CLEAR_FS_LAYOUT: 121 DeleteTableProcedure.deleteFromFs(env, getTableName(), regions, true); 122 // NOTE: It's very important that we create new HRegions before next state, so that 123 // they get persisted in procedure state before we start using them for anything. 124 // Otherwise, if we create them in next step and master crashes after creating fs 125 // layout but before saving state, region re-created after recovery will have different 126 // regionId(s) and encoded names. That will lead to unwanted regions in FS layout 127 // (which were created before the crash). 128 if (!preserveSplits) { 129 // if we are not preserving splits, generate a new single region 130 regions = Arrays.asList(ModifyRegionUtils.createRegionInfos(tableDescriptor, null)); 131 } else { 132 regions = recreateRegionInfo(regions); 133 } 134 setNextState(TruncateTableState.TRUNCATE_TABLE_REMOVE_FROM_META); 135 break; 136 case TRUNCATE_TABLE_REMOVE_FROM_META: 137 List<RegionInfo> originalRegions = 138 env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()); 139 DeleteTableProcedure.deleteFromMeta(env, getTableName(), originalRegions); 140 DeleteTableProcedure.deleteAssignmentState(env, getTableName()); 141 setNextState(TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT); 142 break; 143 case TRUNCATE_TABLE_CREATE_FS_LAYOUT: 144 DeleteTableProcedure.deleteFromFs(env, getTableName(), regions, true); 145 regions = CreateTableProcedure.createFsLayout(env, tableDescriptor, regions); 146 env.getMasterServices().getTableDescriptors().update(tableDescriptor, true); 147 setNextState(TruncateTableState.TRUNCATE_TABLE_ADD_TO_META); 148 break; 149 case TRUNCATE_TABLE_ADD_TO_META: 150 regions = CreateTableProcedure.addTableToMeta(env, tableDescriptor, regions); 151 setNextState(TruncateTableState.TRUNCATE_TABLE_ASSIGN_REGIONS); 152 break; 153 case TRUNCATE_TABLE_ASSIGN_REGIONS: 154 CreateTableProcedure.setEnablingState(env, getTableName()); 155 addChildProcedure(env.getAssignmentManager().createRoundRobinAssignProcedures(regions)); 156 setNextState(TruncateTableState.TRUNCATE_TABLE_POST_OPERATION); 157 tableDescriptor = null; 158 regions = null; 159 break; 160 case TRUNCATE_TABLE_POST_OPERATION: 161 CreateTableProcedure.setEnabledState(env, getTableName()); 162 postTruncate(env); 163 LOG.debug("truncate '" + getTableName() + "' completed"); 164 return Flow.NO_MORE_STATE; 165 default: 166 throw new UnsupportedOperationException("unhandled state=" + state); 167 } 168 } catch (IOException e) { 169 if (isRollbackSupported(state)) { 170 setFailure("master-truncate-table", e); 171 } else { 172 LOG.warn("Retriable error trying to truncate table=" + getTableName() + " state=" + state, 173 e); 174 } 175 } 176 return Flow.HAS_MORE_STATE; 177 } 178 179 @Override 180 protected void rollbackState(final MasterProcedureEnv env, final TruncateTableState state) { 181 switch (state) { 182 case TRUNCATE_TABLE_PRE_OPERATION: 183 // nothing to rollback, pre-truncate is just table-state checks. 184 // We can fail if the table does not exist or is not disabled. 185 // TODO: coprocessor rollback semantic is still undefined. 186 break; 187 case TRUNCATE_TABLE_SNAPSHOT: 188 // Handle recovery snapshot rollback. There is no DeleteSnapshotProcedure as such to use 189 // here directly as a child procedure, so we call a utility method to delete the snapshot 190 // which uses the SnapshotManager to delete the snapshot. 191 if (recoverySnapshotName != null) { 192 RecoverySnapshotUtils.deleteRecoverySnapshot(env, recoverySnapshotName, tableName); 193 recoverySnapshotName = null; 194 } 195 break; 196 default: 197 // Truncate from other states doesn't have a rollback. The execution will succeed, at some 198 // point. 199 throw new UnsupportedOperationException("unhandled state=" + state); 200 } 201 } 202 203 @Override 204 protected void completionCleanup(final MasterProcedureEnv env) { 205 releaseSyncLatch(); 206 } 207 208 @Override 209 protected boolean isRollbackSupported(final TruncateTableState state) { 210 switch (state) { 211 case TRUNCATE_TABLE_PRE_OPERATION: 212 case TRUNCATE_TABLE_SNAPSHOT: 213 return true; 214 default: 215 return false; 216 } 217 } 218 219 @Override 220 protected TruncateTableState getState(final int stateId) { 221 return TruncateTableState.forNumber(stateId); 222 } 223 224 @Override 225 protected int getStateId(final TruncateTableState state) { 226 return state.getNumber(); 227 } 228 229 @Override 230 protected TruncateTableState getInitialState() { 231 return TruncateTableState.TRUNCATE_TABLE_PRE_OPERATION; 232 } 233 234 @Override 235 protected boolean holdLock(MasterProcedureEnv env) { 236 return true; 237 } 238 239 @Override 240 public TableName getTableName() { 241 return tableName; 242 } 243 244 @Override 245 public TableOperationType getTableOperationType() { 246 return TableOperationType.EDIT; 247 } 248 249 @Override 250 public void toStringClassDetails(StringBuilder sb) { 251 sb.append(getClass().getSimpleName()); 252 sb.append(" (table="); 253 sb.append(getTableName()); 254 sb.append(" preserveSplits="); 255 sb.append(preserveSplits); 256 sb.append(")"); 257 } 258 259 @Override 260 protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { 261 super.serializeStateData(serializer); 262 263 MasterProcedureProtos.TruncateTableStateData.Builder state = 264 MasterProcedureProtos.TruncateTableStateData.newBuilder() 265 .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser())) 266 .setPreserveSplits(preserveSplits); 267 if (tableDescriptor != null) { 268 state.setTableSchema(ProtobufUtil.toTableSchema(tableDescriptor)); 269 } else { 270 state.setTableName(ProtobufUtil.toProtoTableName(tableName)); 271 } 272 if (regions != null) { 273 for (RegionInfo hri : regions) { 274 state.addRegionInfo(ProtobufUtil.toRegionInfo(hri)); 275 } 276 } 277 if (recoverySnapshotName != null) { 278 state.setSnapshotName(recoverySnapshotName); 279 } 280 serializer.serialize(state.build()); 281 } 282 283 @Override 284 protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { 285 super.deserializeStateData(serializer); 286 287 MasterProcedureProtos.TruncateTableStateData state = 288 serializer.deserialize(MasterProcedureProtos.TruncateTableStateData.class); 289 setUser(MasterProcedureUtil.toUserInfo(state.getUserInfo())); 290 if (state.hasTableSchema()) { 291 tableDescriptor = ProtobufUtil.toTableDescriptor(state.getTableSchema()); 292 tableName = tableDescriptor.getTableName(); 293 } else { 294 tableName = ProtobufUtil.toTableName(state.getTableName()); 295 } 296 preserveSplits = state.getPreserveSplits(); 297 if (state.getRegionInfoCount() == 0) { 298 regions = null; 299 } else { 300 regions = new ArrayList<>(state.getRegionInfoCount()); 301 for (HBaseProtos.RegionInfo hri : state.getRegionInfoList()) { 302 regions.add(ProtobufUtil.toRegionInfo(hri)); 303 } 304 } 305 if (state.hasSnapshotName()) { 306 recoverySnapshotName = state.getSnapshotName(); 307 } 308 } 309 310 private static List<RegionInfo> recreateRegionInfo(final List<RegionInfo> regions) { 311 ArrayList<RegionInfo> newRegions = new ArrayList<>(regions.size()); 312 for (RegionInfo hri : regions) { 313 newRegions.add(RegionInfoBuilder.newBuilder(hri.getTable()).setStartKey(hri.getStartKey()) 314 .setEndKey(hri.getEndKey()).build()); 315 } 316 return newRegions; 317 } 318 319 private boolean prepareTruncate(final MasterProcedureEnv env) throws IOException { 320 try { 321 env.getMasterServices().checkTableModifiable(getTableName()); 322 } catch (TableNotFoundException | TableNotDisabledException e) { 323 setFailure("master-truncate-table", e); 324 return false; 325 } 326 return true; 327 } 328 329 private boolean preTruncate(final MasterProcedureEnv env) 330 throws IOException, InterruptedException { 331 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 332 if (cpHost != null) { 333 final TableName tableName = getTableName(); 334 cpHost.preTruncateTableAction(tableName, getUser()); 335 } 336 return true; 337 } 338 339 private void postTruncate(final MasterProcedureEnv env) throws IOException, InterruptedException { 340 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 341 if (cpHost != null) { 342 final TableName tableName = getTableName(); 343 cpHost.postCompletedTruncateTableAction(tableName, getUser()); 344 } 345 } 346 347 RegionInfo getFirstRegionInfo() { 348 if (regions == null || regions.isEmpty()) { 349 return null; 350 } 351 return regions.get(0); 352 } 353}