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.function.Supplier; 026import java.util.stream.Collectors; 027import java.util.stream.IntStream; 028import org.apache.hadoop.hbase.ConcurrentTableModificationException; 029import org.apache.hadoop.hbase.DoNotRetryIOException; 030import org.apache.hadoop.hbase.HBaseIOException; 031import org.apache.hadoop.hbase.HConstants; 032import org.apache.hadoop.hbase.TableName; 033import org.apache.hadoop.hbase.TableNotFoundException; 034import org.apache.hadoop.hbase.client.RegionInfo; 035import org.apache.hadoop.hbase.client.RegionReplicaUtil; 036import org.apache.hadoop.hbase.client.TableDescriptor; 037import org.apache.hadoop.hbase.master.MasterCoprocessorHost; 038import org.apache.hadoop.hbase.master.zksyncer.MetaLocationSyncer; 039import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer; 040import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerValidationUtils; 041import org.apache.hadoop.hbase.rsgroup.RSGroupInfo; 042import org.apache.hadoop.hbase.util.Bytes; 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 */ 283 private void prepareModify(final MasterProcedureEnv env) throws IOException { 284 // Checks whether the table exists 285 if (!env.getMasterServices().getTableDescriptors().exists(getTableName())) { 286 throw new TableNotFoundException(getTableName()); 287 } 288 289 // check that we have at least 1 CF 290 if (modifiedTableDescriptor.getColumnFamilyCount() == 0) { 291 throw new DoNotRetryIOException( 292 "Table " + getTableName().toString() + " should have at least one column family."); 293 } 294 295 // If descriptor check is enabled, check whether the table descriptor when procedure was 296 // submitted matches with the current 297 // table descriptor of the table, else retrieve the old descriptor 298 // for comparison in order to update the descriptor. 299 if (shouldCheckDescriptor) { 300 if ( 301 TableDescriptor.COMPARATOR.compare(unmodifiedTableDescriptor, 302 env.getMasterServices().getTableDescriptors().get(getTableName())) != 0 303 ) { 304 LOG.error("Error while modifying table '" + getTableName().toString() 305 + "' Skipping procedure : " + this); 306 throw new ConcurrentTableModificationException( 307 "Skipping modify table operation on table '" + getTableName().toString() 308 + "' as it has already been modified by some other concurrent operation, " 309 + "Please retry."); 310 } 311 } else { 312 this.unmodifiedTableDescriptor = 313 env.getMasterServices().getTableDescriptors().get(getTableName()); 314 } 315 316 this.deleteColumnFamilyInModify = 317 isDeleteColumnFamily(unmodifiedTableDescriptor, modifiedTableDescriptor); 318 if ( 319 !unmodifiedTableDescriptor.getRegionServerGroup() 320 .equals(modifiedTableDescriptor.getRegionServerGroup()) 321 ) { 322 Supplier<String> forWhom = () -> "table " + getTableName(); 323 RSGroupInfo rsGroupInfo = MasterProcedureUtil.checkGroupExists( 324 env.getMasterServices().getRSGroupInfoManager()::getRSGroup, 325 modifiedTableDescriptor.getRegionServerGroup(), forWhom); 326 MasterProcedureUtil.checkGroupNotEmpty(rsGroupInfo, forWhom); 327 } 328 329 // check for store file tracker configurations 330 StoreFileTrackerValidationUtils.checkForModifyTable(env.getMasterConfiguration(), 331 unmodifiedTableDescriptor, modifiedTableDescriptor, !isTableEnabled(env)); 332 } 333 334 /** 335 * Find out whether all column families in unmodifiedTableDescriptor also exists in the 336 * modifiedTableDescriptor. 337 * @return True if we are deleting a column family. 338 */ 339 private static boolean isDeleteColumnFamily(TableDescriptor originalDescriptor, 340 TableDescriptor newDescriptor) { 341 boolean result = false; 342 final Set<byte[]> originalFamilies = originalDescriptor.getColumnFamilyNames(); 343 final Set<byte[]> newFamilies = newDescriptor.getColumnFamilyNames(); 344 for (byte[] familyName : originalFamilies) { 345 if (!newFamilies.contains(familyName)) { 346 result = true; 347 break; 348 } 349 } 350 return result; 351 } 352 353 /** 354 * Action before modifying table. 355 * @param env MasterProcedureEnv 356 * @param state the procedure state 357 */ 358 private void preModify(final MasterProcedureEnv env, final ModifyTableState state) 359 throws IOException, InterruptedException { 360 runCoprocessorAction(env, state); 361 } 362 363 /** 364 * Update descriptor 365 * @param env MasterProcedureEnv 366 **/ 367 private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException { 368 env.getMasterServices().getTableDescriptors().update(modifiedTableDescriptor); 369 } 370 371 /** 372 * Removes from hdfs the families that are not longer present in the new table descriptor. 373 * @param env MasterProcedureEnv 374 */ 375 private void deleteFromFs(final MasterProcedureEnv env, final TableDescriptor oldTableDescriptor, 376 final TableDescriptor newTableDescriptor) throws IOException { 377 final Set<byte[]> oldFamilies = oldTableDescriptor.getColumnFamilyNames(); 378 final Set<byte[]> newFamilies = newTableDescriptor.getColumnFamilyNames(); 379 for (byte[] familyName : oldFamilies) { 380 if (!newFamilies.contains(familyName)) { 381 MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(env, getTableName(), 382 getRegionInfoList(env), familyName, 383 oldTableDescriptor.getColumnFamily(familyName).isMobEnabled()); 384 } 385 } 386 } 387 388 /** 389 * remove replica columns if necessary. 390 */ 391 private void removeReplicaColumnsIfNeeded(MasterProcedureEnv env) throws IOException { 392 final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication(); 393 final int newReplicaCount = modifiedTableDescriptor.getRegionReplication(); 394 if (newReplicaCount >= oldReplicaCount) { 395 return; 396 } 397 env.getAssignmentManager().getRegionStateStore().removeRegionReplicas(getTableName(), 398 oldReplicaCount, newReplicaCount); 399 env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()).stream() 400 .filter(r -> r.getReplicaId() >= newReplicaCount) 401 .forEach(env.getAssignmentManager().getRegionStates()::deleteRegion); 402 } 403 404 private void assignNewReplicasIfNeeded(MasterProcedureEnv env) throws IOException { 405 final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication(); 406 final int newReplicaCount = modifiedTableDescriptor.getRegionReplication(); 407 if (newReplicaCount <= oldReplicaCount) { 408 return; 409 } 410 if (isTableEnabled(env)) { 411 List<RegionInfo> newReplicas = env.getAssignmentManager().getRegionStates() 412 .getRegionsOfTable(getTableName()).stream().filter(RegionReplicaUtil::isDefaultReplica) 413 .flatMap(primaryRegion -> IntStream.range(oldReplicaCount, newReplicaCount).mapToObj( 414 replicaId -> RegionReplicaUtil.getRegionInfoForReplica(primaryRegion, replicaId))) 415 .collect(Collectors.toList()); 416 addChildProcedure(env.getAssignmentManager().createAssignProcedures(newReplicas)); 417 } 418 } 419 420 private void closeExcessReplicasIfNeeded(MasterProcedureEnv env) { 421 final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication(); 422 final int newReplicaCount = modifiedTableDescriptor.getRegionReplication(); 423 if (newReplicaCount >= oldReplicaCount) { 424 return; 425 } 426 addChildProcedure(env.getAssignmentManager() 427 .createUnassignProceduresForClosingExcessRegionReplicas(getTableName(), newReplicaCount)); 428 } 429 430 /** 431 * Action after modifying table. 432 * @param env MasterProcedureEnv 433 * @param state the procedure state 434 */ 435 private void postModify(final MasterProcedureEnv env, final ModifyTableState state) 436 throws IOException, InterruptedException { 437 runCoprocessorAction(env, state); 438 } 439 440 /** 441 * Coprocessor Action. 442 * @param env MasterProcedureEnv 443 * @param state the procedure state 444 */ 445 private void runCoprocessorAction(final MasterProcedureEnv env, final ModifyTableState state) 446 throws IOException, InterruptedException { 447 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 448 if (cpHost != null) { 449 switch (state) { 450 case MODIFY_TABLE_PRE_OPERATION: 451 cpHost.preModifyTableAction(getTableName(), unmodifiedTableDescriptor, 452 modifiedTableDescriptor, getUser()); 453 break; 454 case MODIFY_TABLE_POST_OPERATION: 455 cpHost.postCompletedModifyTableAction(getTableName(), unmodifiedTableDescriptor, 456 modifiedTableDescriptor, getUser()); 457 break; 458 default: 459 throw new UnsupportedOperationException(this + " unhandled state=" + state); 460 } 461 } 462 } 463 464 /** 465 * Fetches all Regions for a table. Cache the result of this method if you need to use it multiple 466 * times. Be aware that it may change over in between calls to this procedure. 467 */ 468 private List<RegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException { 469 return env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()); 470 } 471}