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.Arrays; 022import java.util.Collections; 023import java.util.List; 024import java.util.Set; 025import java.util.stream.Collectors; 026import java.util.stream.IntStream; 027import org.apache.hadoop.hbase.ConcurrentTableModificationException; 028import org.apache.hadoop.hbase.DoNotRetryIOException; 029import org.apache.hadoop.hbase.HBaseIOException; 030import org.apache.hadoop.hbase.HConstants; 031import org.apache.hadoop.hbase.TableName; 032import org.apache.hadoop.hbase.TableNotFoundException; 033import org.apache.hadoop.hbase.client.RegionInfo; 034import org.apache.hadoop.hbase.client.RegionReplicaUtil; 035import org.apache.hadoop.hbase.client.TableDescriptor; 036import org.apache.hadoop.hbase.master.MasterCoprocessorHost; 037import org.apache.hadoop.hbase.master.zksyncer.MetaLocationSyncer; 038import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer; 039import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerValidationUtils; 040import org.apache.hadoop.hbase.replication.ReplicationException; 041import org.apache.hadoop.hbase.util.Bytes; 042import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil; 043import org.apache.yetus.audience.InterfaceAudience; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; 048import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos; 049import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.ModifyTableState; 050 051@InterfaceAudience.Private 052public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<ModifyTableState> { 053 private static final Logger LOG = LoggerFactory.getLogger(ModifyTableProcedure.class); 054 055 private TableDescriptor unmodifiedTableDescriptor = null; 056 private TableDescriptor modifiedTableDescriptor; 057 private boolean deleteColumnFamilyInModify; 058 private boolean shouldCheckDescriptor; 059 /** 060 * List of column families that cannot be deleted from the hbase:meta table. They are critical to 061 * cluster operation. This is a bit of an odd place to keep this list but then this is the tooling 062 * that does add/remove. Keeping it local! 063 */ 064 private static final List<byte[]> UNDELETABLE_META_COLUMNFAMILIES = 065 Collections.unmodifiableList(Arrays.asList(HConstants.CATALOG_FAMILY, HConstants.TABLE_FAMILY, 066 HConstants.REPLICATION_BARRIER_FAMILY)); 067 068 public ModifyTableProcedure() { 069 super(); 070 initialize(null, false); 071 } 072 073 public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd) 074 throws HBaseIOException { 075 this(env, htd, null); 076 } 077 078 public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd, 079 final ProcedurePrepareLatch latch) throws HBaseIOException { 080 this(env, htd, latch, null, false); 081 } 082 083 public ModifyTableProcedure(final MasterProcedureEnv env, 084 final TableDescriptor newTableDescriptor, final ProcedurePrepareLatch latch, 085 final TableDescriptor oldTableDescriptor, final boolean shouldCheckDescriptor) 086 throws HBaseIOException { 087 super(env, latch); 088 initialize(oldTableDescriptor, shouldCheckDescriptor); 089 this.modifiedTableDescriptor = newTableDescriptor; 090 preflightChecks(env, null/* No table checks; if changing peers, table can be online */); 091 } 092 093 @Override 094 protected void preflightChecks(MasterProcedureEnv env, Boolean enabled) throws HBaseIOException { 095 super.preflightChecks(env, enabled); 096 if (this.modifiedTableDescriptor.isMetaTable()) { 097 // If we are modifying the hbase:meta table, make sure we are not deleting critical 098 // column families else we'll damage the cluster. 099 Set<byte[]> cfs = this.modifiedTableDescriptor.getColumnFamilyNames(); 100 for (byte[] family : UNDELETABLE_META_COLUMNFAMILIES) { 101 if (!cfs.contains(family)) { 102 throw new HBaseIOException( 103 "Delete of hbase:meta column family " + Bytes.toString(family)); 104 } 105 } 106 } 107 } 108 109 private void initialize(final TableDescriptor unmodifiedTableDescriptor, 110 final boolean shouldCheckDescriptor) { 111 this.unmodifiedTableDescriptor = unmodifiedTableDescriptor; 112 this.shouldCheckDescriptor = shouldCheckDescriptor; 113 this.deleteColumnFamilyInModify = false; 114 } 115 116 @Override 117 protected Flow executeFromState(final MasterProcedureEnv env, final ModifyTableState state) 118 throws InterruptedException { 119 LOG.trace("{} execute state={}", this, state); 120 try { 121 switch (state) { 122 case MODIFY_TABLE_PREPARE: 123 prepareModify(env); 124 setNextState(ModifyTableState.MODIFY_TABLE_PRE_OPERATION); 125 break; 126 case MODIFY_TABLE_PRE_OPERATION: 127 preModify(env, state); 128 setNextState(ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS); 129 break; 130 case MODIFY_TABLE_CLOSE_EXCESS_REPLICAS: 131 if (isTableEnabled(env)) { 132 closeExcessReplicasIfNeeded(env); 133 } 134 setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR); 135 break; 136 case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR: 137 updateTableDescriptor(env); 138 setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN); 139 break; 140 case MODIFY_TABLE_REMOVE_REPLICA_COLUMN: 141 removeReplicaColumnsIfNeeded(env); 142 setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION); 143 break; 144 case MODIFY_TABLE_POST_OPERATION: 145 postModify(env, state); 146 setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS); 147 break; 148 case MODIFY_TABLE_REOPEN_ALL_REGIONS: 149 if (isTableEnabled(env)) { 150 addChildProcedure(new ReopenTableRegionsProcedure(getTableName())); 151 } 152 setNextState(ModifyTableState.MODIFY_TABLE_ASSIGN_NEW_REPLICAS); 153 break; 154 case MODIFY_TABLE_ASSIGN_NEW_REPLICAS: 155 assignNewReplicasIfNeeded(env); 156 if (TableName.isMetaTableName(getTableName())) { 157 MetaLocationSyncer syncer = env.getMasterServices().getMetaLocationSyncer(); 158 if (syncer != null) { 159 syncer.setMetaReplicaCount(modifiedTableDescriptor.getRegionReplication()); 160 } 161 } 162 if (deleteColumnFamilyInModify) { 163 setNextState(ModifyTableState.MODIFY_TABLE_DELETE_FS_LAYOUT); 164 } else { 165 return Flow.NO_MORE_STATE; 166 } 167 break; 168 case MODIFY_TABLE_DELETE_FS_LAYOUT: 169 deleteFromFs(env, unmodifiedTableDescriptor, modifiedTableDescriptor); 170 return Flow.NO_MORE_STATE; 171 default: 172 throw new UnsupportedOperationException("unhandled state=" + state); 173 } 174 } catch (IOException e) { 175 if (isRollbackSupported(state)) { 176 setFailure("master-modify-table", e); 177 } else { 178 LOG.warn("Retriable error trying to modify table={} (in state={})", getTableName(), state, 179 e); 180 } 181 } 182 return Flow.HAS_MORE_STATE; 183 } 184 185 @Override 186 protected void rollbackState(final MasterProcedureEnv env, final ModifyTableState state) 187 throws IOException { 188 if ( 189 state == ModifyTableState.MODIFY_TABLE_PREPARE 190 || state == ModifyTableState.MODIFY_TABLE_PRE_OPERATION 191 ) { 192 // nothing to rollback, pre-modify is just checks. 193 // TODO: coprocessor rollback semantic is still undefined. 194 return; 195 } 196 197 // The delete doesn't have a rollback. The execution will succeed, at some point. 198 throw new UnsupportedOperationException("unhandled state=" + state); 199 } 200 201 @Override 202 protected boolean isRollbackSupported(final ModifyTableState state) { 203 switch (state) { 204 case MODIFY_TABLE_PRE_OPERATION: 205 case MODIFY_TABLE_PREPARE: 206 return true; 207 default: 208 return false; 209 } 210 } 211 212 @Override 213 protected void completionCleanup(final MasterProcedureEnv env) { 214 releaseSyncLatch(); 215 } 216 217 @Override 218 protected ModifyTableState getState(final int stateId) { 219 return ModifyTableState.forNumber(stateId); 220 } 221 222 @Override 223 protected int getStateId(final ModifyTableState state) { 224 return state.getNumber(); 225 } 226 227 @Override 228 protected ModifyTableState getInitialState() { 229 return ModifyTableState.MODIFY_TABLE_PREPARE; 230 } 231 232 @Override 233 protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { 234 super.serializeStateData(serializer); 235 236 MasterProcedureProtos.ModifyTableStateData.Builder modifyTableMsg = 237 MasterProcedureProtos.ModifyTableStateData.newBuilder() 238 .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser())) 239 .setModifiedTableSchema(ProtobufUtil.toTableSchema(modifiedTableDescriptor)) 240 .setDeleteColumnFamilyInModify(deleteColumnFamilyInModify) 241 .setShouldCheckDescriptor(shouldCheckDescriptor); 242 243 if (unmodifiedTableDescriptor != null) { 244 modifyTableMsg 245 .setUnmodifiedTableSchema(ProtobufUtil.toTableSchema(unmodifiedTableDescriptor)); 246 } 247 248 serializer.serialize(modifyTableMsg.build()); 249 } 250 251 @Override 252 protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { 253 super.deserializeStateData(serializer); 254 255 MasterProcedureProtos.ModifyTableStateData modifyTableMsg = 256 serializer.deserialize(MasterProcedureProtos.ModifyTableStateData.class); 257 setUser(MasterProcedureUtil.toUserInfo(modifyTableMsg.getUserInfo())); 258 modifiedTableDescriptor = 259 ProtobufUtil.toTableDescriptor(modifyTableMsg.getModifiedTableSchema()); 260 deleteColumnFamilyInModify = modifyTableMsg.getDeleteColumnFamilyInModify(); 261 shouldCheckDescriptor = 262 modifyTableMsg.hasShouldCheckDescriptor() ? modifyTableMsg.getShouldCheckDescriptor() : false; 263 264 if (modifyTableMsg.hasUnmodifiedTableSchema()) { 265 unmodifiedTableDescriptor = 266 ProtobufUtil.toTableDescriptor(modifyTableMsg.getUnmodifiedTableSchema()); 267 } 268 } 269 270 @Override 271 public TableName getTableName() { 272 return modifiedTableDescriptor.getTableName(); 273 } 274 275 @Override 276 public TableOperationType getTableOperationType() { 277 return TableOperationType.EDIT; 278 } 279 280 /** 281 * Check conditions before any real action of modifying a table. 282 * @param env MasterProcedureEnv n 283 */ 284 private void prepareModify(final MasterProcedureEnv env) throws IOException { 285 // Checks whether the table exists 286 if (!env.getMasterServices().getTableDescriptors().exists(getTableName())) { 287 throw new TableNotFoundException(getTableName()); 288 } 289 290 // check that we have at least 1 CF 291 if (modifiedTableDescriptor.getColumnFamilyCount() == 0) { 292 throw new DoNotRetryIOException( 293 "Table " + getTableName().toString() + " should have at least one column family."); 294 } 295 296 // If descriptor check is enabled, check whether the table descriptor when procedure was 297 // submitted matches with the current 298 // table descriptor of the table, else retrieve the old descriptor 299 // for comparison in order to update the descriptor. 300 if (shouldCheckDescriptor) { 301 if ( 302 TableDescriptor.COMPARATOR.compare(unmodifiedTableDescriptor, 303 env.getMasterServices().getTableDescriptors().get(getTableName())) != 0 304 ) { 305 LOG.error("Error while modifying table '" + getTableName().toString() 306 + "' Skipping procedure : " + this); 307 throw new ConcurrentTableModificationException( 308 "Skipping modify table operation on table '" + getTableName().toString() 309 + "' as it has already been modified by some other concurrent operation, " 310 + "Please retry."); 311 } 312 } else { 313 this.unmodifiedTableDescriptor = 314 env.getMasterServices().getTableDescriptors().get(getTableName()); 315 } 316 317 this.deleteColumnFamilyInModify = 318 isDeleteColumnFamily(unmodifiedTableDescriptor, modifiedTableDescriptor); 319 320 // check for store file tracker configurations 321 StoreFileTrackerValidationUtils.checkForModifyTable(env.getMasterConfiguration(), 322 unmodifiedTableDescriptor, modifiedTableDescriptor, !isTableEnabled(env)); 323 } 324 325 /** 326 * Find out whether all column families in unmodifiedTableDescriptor also exists in the 327 * modifiedTableDescriptor. 328 * @return True if we are deleting a column family. 329 */ 330 private static boolean isDeleteColumnFamily(TableDescriptor originalDescriptor, 331 TableDescriptor newDescriptor) { 332 boolean result = false; 333 final Set<byte[]> originalFamilies = originalDescriptor.getColumnFamilyNames(); 334 final Set<byte[]> newFamilies = newDescriptor.getColumnFamilyNames(); 335 for (byte[] familyName : originalFamilies) { 336 if (!newFamilies.contains(familyName)) { 337 result = true; 338 break; 339 } 340 } 341 return result; 342 } 343 344 /** 345 * Action before modifying table. 346 * @param env MasterProcedureEnv 347 * @param state the procedure state 348 */ 349 private void preModify(final MasterProcedureEnv env, final ModifyTableState state) 350 throws IOException, InterruptedException { 351 runCoprocessorAction(env, state); 352 } 353 354 /** 355 * Update descriptor 356 * @param env MasterProcedureEnv 357 **/ 358 private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException { 359 env.getMasterServices().getTableDescriptors().update(modifiedTableDescriptor); 360 } 361 362 /** 363 * Removes from hdfs the families that are not longer present in the new table descriptor. 364 * @param env MasterProcedureEnv 365 */ 366 private void deleteFromFs(final MasterProcedureEnv env, final TableDescriptor oldTableDescriptor, 367 final TableDescriptor newTableDescriptor) throws IOException { 368 final Set<byte[]> oldFamilies = oldTableDescriptor.getColumnFamilyNames(); 369 final Set<byte[]> newFamilies = newTableDescriptor.getColumnFamilyNames(); 370 for (byte[] familyName : oldFamilies) { 371 if (!newFamilies.contains(familyName)) { 372 MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(env, getTableName(), 373 getRegionInfoList(env), familyName, 374 oldTableDescriptor.getColumnFamily(familyName).isMobEnabled()); 375 } 376 } 377 } 378 379 /** 380 * remove replica columns if necessary. 381 */ 382 private void removeReplicaColumnsIfNeeded(MasterProcedureEnv env) throws IOException { 383 final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication(); 384 final int newReplicaCount = modifiedTableDescriptor.getRegionReplication(); 385 if (newReplicaCount >= oldReplicaCount) { 386 return; 387 } 388 env.getAssignmentManager().getRegionStateStore().removeRegionReplicas(getTableName(), 389 oldReplicaCount, newReplicaCount); 390 env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()).stream() 391 .filter(r -> r.getReplicaId() >= newReplicaCount) 392 .forEach(env.getAssignmentManager().getRegionStates()::deleteRegion); 393 } 394 395 private void assignNewReplicasIfNeeded(MasterProcedureEnv env) throws IOException { 396 final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication(); 397 final int newReplicaCount = modifiedTableDescriptor.getRegionReplication(); 398 if (newReplicaCount <= oldReplicaCount) { 399 return; 400 } 401 if (isTableEnabled(env)) { 402 List<RegionInfo> newReplicas = env.getAssignmentManager().getRegionStates() 403 .getRegionsOfTable(getTableName()).stream().filter(RegionReplicaUtil::isDefaultReplica) 404 .flatMap(primaryRegion -> IntStream.range(oldReplicaCount, newReplicaCount).mapToObj( 405 replicaId -> RegionReplicaUtil.getRegionInfoForReplica(primaryRegion, replicaId))) 406 .collect(Collectors.toList()); 407 addChildProcedure(env.getAssignmentManager().createAssignProcedures(newReplicas)); 408 } 409 if (oldReplicaCount <= 1) { 410 try { 411 ServerRegionReplicaUtil.setupRegionReplicaReplication(env.getMasterServices()); 412 } catch (ReplicationException e) { 413 throw new HBaseIOException(e); 414 } 415 } 416 } 417 418 private void closeExcessReplicasIfNeeded(MasterProcedureEnv env) { 419 final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication(); 420 final int newReplicaCount = modifiedTableDescriptor.getRegionReplication(); 421 if (newReplicaCount >= oldReplicaCount) { 422 return; 423 } 424 addChildProcedure(env.getAssignmentManager() 425 .createUnassignProceduresForClosingExcessRegionReplicas(getTableName(), newReplicaCount)); 426 } 427 428 /** 429 * Action after modifying table. 430 * @param env MasterProcedureEnv 431 * @param state the procedure state 432 */ 433 private void postModify(final MasterProcedureEnv env, final ModifyTableState state) 434 throws IOException, InterruptedException { 435 runCoprocessorAction(env, state); 436 } 437 438 /** 439 * Coprocessor Action. 440 * @param env MasterProcedureEnv 441 * @param state the procedure state 442 */ 443 private void runCoprocessorAction(final MasterProcedureEnv env, final ModifyTableState state) 444 throws IOException, InterruptedException { 445 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 446 if (cpHost != null) { 447 switch (state) { 448 case MODIFY_TABLE_PRE_OPERATION: 449 cpHost.preModifyTableAction(getTableName(), unmodifiedTableDescriptor, 450 modifiedTableDescriptor, getUser()); 451 break; 452 case MODIFY_TABLE_POST_OPERATION: 453 cpHost.postCompletedModifyTableAction(getTableName(), unmodifiedTableDescriptor, 454 modifiedTableDescriptor, getUser()); 455 break; 456 default: 457 throw new UnsupportedOperationException(this + " unhandled state=" + state); 458 } 459 } 460 } 461 462 /** 463 * Fetches all Regions for a table. Cache the result of this method if you need to use it multiple 464 * times. Be aware that it may change over in between calls to this procedure. 465 */ 466 private List<RegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException { 467 return env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()); 468 } 469}