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 */ 018 019package org.apache.hadoop.hbase.master.procedure; 020 021import java.io.IOException; 022import java.util.Arrays; 023import java.util.Collections; 024import java.util.List; 025import java.util.Set; 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.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 053 extends AbstractStateMachineTableProcedure<ModifyTableState> { 054 private static final Logger LOG = LoggerFactory.getLogger(ModifyTableProcedure.class); 055 056 private TableDescriptor unmodifiedTableDescriptor = null; 057 private TableDescriptor modifiedTableDescriptor; 058 private boolean deleteColumnFamilyInModify; 059 private boolean shouldCheckDescriptor; 060 /** 061 * List of column families that cannot be deleted from the hbase:meta table. 062 * They are critical to cluster operation. This is a bit of an odd place to 063 * keep this list but then this is the tooling that does add/remove. Keeping 064 * it local! 065 */ 066 private static final List<byte []> UNDELETABLE_META_COLUMNFAMILIES = 067 Collections.unmodifiableList(Arrays.asList( 068 HConstants.CATALOG_FAMILY, HConstants.TABLE_FAMILY, HConstants.REPLICATION_BARRIER_FAMILY)); 069 070 public ModifyTableProcedure() { 071 super(); 072 initialize(null, false); 073 } 074 075 public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd) 076 throws HBaseIOException { 077 this(env, htd, null); 078 } 079 080 public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd, 081 final ProcedurePrepareLatch latch) 082 throws HBaseIOException { 083 this(env, htd, latch, null, false); 084 } 085 086 public ModifyTableProcedure(final MasterProcedureEnv env, 087 final TableDescriptor newTableDescriptor, final ProcedurePrepareLatch latch, 088 final TableDescriptor oldTableDescriptor, final boolean shouldCheckDescriptor) 089 throws HBaseIOException { 090 super(env, latch); 091 initialize(oldTableDescriptor, shouldCheckDescriptor); 092 this.modifiedTableDescriptor = newTableDescriptor; 093 preflightChecks(env, null/*No table checks; if changing peers, table can be online*/); 094 } 095 096 @Override 097 protected void preflightChecks(MasterProcedureEnv env, Boolean enabled) throws HBaseIOException { 098 super.preflightChecks(env, enabled); 099 if (this.modifiedTableDescriptor.isMetaTable()) { 100 // If we are modifying the hbase:meta table, make sure we are not deleting critical 101 // column families else we'll damage the cluster. 102 Set<byte []> cfs = this.modifiedTableDescriptor.getColumnFamilyNames(); 103 for (byte[] family : UNDELETABLE_META_COLUMNFAMILIES) { 104 if (!cfs.contains(family)) { 105 throw new HBaseIOException("Delete of hbase:meta column family " + 106 Bytes.toString(family)); 107 } 108 } 109 } 110 } 111 112 private void initialize(final TableDescriptor unmodifiedTableDescriptor, 113 final boolean shouldCheckDescriptor) { 114 this.unmodifiedTableDescriptor = unmodifiedTableDescriptor; 115 this.shouldCheckDescriptor = shouldCheckDescriptor; 116 this.deleteColumnFamilyInModify = false; 117 } 118 119 @Override 120 protected Flow executeFromState(final MasterProcedureEnv env, final ModifyTableState state) 121 throws InterruptedException { 122 LOG.trace("{} execute state={}", this, state); 123 try { 124 switch (state) { 125 case MODIFY_TABLE_PREPARE: 126 prepareModify(env); 127 setNextState(ModifyTableState.MODIFY_TABLE_PRE_OPERATION); 128 break; 129 case MODIFY_TABLE_PRE_OPERATION: 130 preModify(env, state); 131 setNextState(ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS); 132 break; 133 case MODIFY_TABLE_CLOSE_EXCESS_REPLICAS: 134 if (isTableEnabled(env)) { 135 closeExcessReplicasIfNeeded(env); 136 } 137 setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR); 138 break; 139 case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR: 140 updateTableDescriptor(env); 141 setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN); 142 break; 143 case MODIFY_TABLE_REMOVE_REPLICA_COLUMN: 144 removeReplicaColumnsIfNeeded(env); 145 setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION); 146 break; 147 case MODIFY_TABLE_POST_OPERATION: 148 postModify(env, state); 149 setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS); 150 break; 151 case MODIFY_TABLE_REOPEN_ALL_REGIONS: 152 if (isTableEnabled(env)) { 153 addChildProcedure(new ReopenTableRegionsProcedure(getTableName())); 154 } 155 setNextState(ModifyTableState.MODIFY_TABLE_ASSIGN_NEW_REPLICAS); 156 break; 157 case MODIFY_TABLE_ASSIGN_NEW_REPLICAS: 158 assignNewReplicasIfNeeded(env); 159 if (TableName.isMetaTableName(getTableName())) { 160 MetaLocationSyncer syncer = env.getMasterServices().getMetaLocationSyncer(); 161 if (syncer != null) { 162 syncer.setMetaReplicaCount(modifiedTableDescriptor.getRegionReplication()); 163 } 164 } 165 if (deleteColumnFamilyInModify) { 166 setNextState(ModifyTableState.MODIFY_TABLE_DELETE_FS_LAYOUT); 167 } else { 168 return Flow.NO_MORE_STATE; 169 } 170 break; 171 case MODIFY_TABLE_DELETE_FS_LAYOUT: 172 deleteFromFs(env, unmodifiedTableDescriptor, modifiedTableDescriptor); 173 return Flow.NO_MORE_STATE; 174 default: 175 throw new UnsupportedOperationException("unhandled state=" + state); 176 } 177 } catch (IOException e) { 178 if (isRollbackSupported(state)) { 179 setFailure("master-modify-table", e); 180 } else { 181 LOG.warn("Retriable error trying to modify table={} (in state={})", getTableName(), state, 182 e); 183 } 184 } 185 return Flow.HAS_MORE_STATE; 186 } 187 188 @Override 189 protected void rollbackState(final MasterProcedureEnv env, final ModifyTableState state) 190 throws IOException { 191 if (state == ModifyTableState.MODIFY_TABLE_PREPARE || 192 state == ModifyTableState.MODIFY_TABLE_PRE_OPERATION) { 193 // nothing to rollback, pre-modify is just checks. 194 // TODO: coprocessor rollback semantic is still undefined. 195 return; 196 } 197 198 // The delete doesn't have a rollback. The execution will succeed, at some point. 199 throw new UnsupportedOperationException("unhandled state=" + state); 200 } 201 202 @Override 203 protected boolean isRollbackSupported(final ModifyTableState state) { 204 switch (state) { 205 case MODIFY_TABLE_PRE_OPERATION: 206 case MODIFY_TABLE_PREPARE: 207 return true; 208 default: 209 return false; 210 } 211 } 212 213 @Override 214 protected void completionCleanup(final MasterProcedureEnv env) { 215 releaseSyncLatch(); 216 } 217 218 @Override 219 protected ModifyTableState getState(final int stateId) { 220 return ModifyTableState.forNumber(stateId); 221 } 222 223 @Override 224 protected int getStateId(final ModifyTableState state) { 225 return state.getNumber(); 226 } 227 228 @Override 229 protected ModifyTableState getInitialState() { 230 return ModifyTableState.MODIFY_TABLE_PREPARE; 231 } 232 233 @Override 234 protected void serializeStateData(ProcedureStateSerializer serializer) 235 throws IOException { 236 super.serializeStateData(serializer); 237 238 MasterProcedureProtos.ModifyTableStateData.Builder modifyTableMsg = 239 MasterProcedureProtos.ModifyTableStateData.newBuilder() 240 .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser())) 241 .setModifiedTableSchema(ProtobufUtil.toTableSchema(modifiedTableDescriptor)) 242 .setDeleteColumnFamilyInModify(deleteColumnFamilyInModify) 243 .setShouldCheckDescriptor(shouldCheckDescriptor); 244 245 if (unmodifiedTableDescriptor != null) { 246 modifyTableMsg 247 .setUnmodifiedTableSchema(ProtobufUtil.toTableSchema(unmodifiedTableDescriptor)); 248 } 249 250 serializer.serialize(modifyTableMsg.build()); 251 } 252 253 @Override 254 protected void deserializeStateData(ProcedureStateSerializer serializer) 255 throws IOException { 256 super.deserializeStateData(serializer); 257 258 MasterProcedureProtos.ModifyTableStateData modifyTableMsg = 259 serializer.deserialize(MasterProcedureProtos.ModifyTableStateData.class); 260 setUser(MasterProcedureUtil.toUserInfo(modifyTableMsg.getUserInfo())); 261 modifiedTableDescriptor = ProtobufUtil.toTableDescriptor(modifyTableMsg.getModifiedTableSchema()); 262 deleteColumnFamilyInModify = modifyTableMsg.getDeleteColumnFamilyInModify(); 263 shouldCheckDescriptor = modifyTableMsg.hasShouldCheckDescriptor() 264 ? modifyTableMsg.getShouldCheckDescriptor() : false; 265 266 if (modifyTableMsg.hasUnmodifiedTableSchema()) { 267 unmodifiedTableDescriptor = 268 ProtobufUtil.toTableDescriptor(modifyTableMsg.getUnmodifiedTableSchema()); 269 } 270 } 271 272 @Override 273 public TableName getTableName() { 274 return modifiedTableDescriptor.getTableName(); 275 } 276 277 @Override 278 public TableOperationType getTableOperationType() { 279 return TableOperationType.EDIT; 280 } 281 282 /** 283 * Check conditions before any real action of modifying a table. 284 * @param env MasterProcedureEnv 285 * @throws IOException 286 */ 287 private void prepareModify(final MasterProcedureEnv env) throws IOException { 288 // Checks whether the table exists 289 if (!env.getMasterServices().getTableDescriptors().exists(getTableName())) { 290 throw new TableNotFoundException(getTableName()); 291 } 292 293 // check that we have at least 1 CF 294 if (modifiedTableDescriptor.getColumnFamilyCount() == 0) { 295 throw new DoNotRetryIOException("Table " + getTableName().toString() + 296 " should have at least one column family."); 297 } 298 299 // If descriptor check is enabled, check whether the table descriptor when procedure was 300 // submitted matches with the current 301 // table descriptor of the table, else retrieve the old descriptor 302 // for comparison in order to update the descriptor. 303 if (shouldCheckDescriptor) { 304 if (TableDescriptor.COMPARATOR.compare(unmodifiedTableDescriptor, 305 env.getMasterServices().getTableDescriptors().get(getTableName())) != 0) { 306 LOG.error("Error while modifying table '" + getTableName().toString() 307 + "' Skipping procedure : " + this); 308 throw new ConcurrentTableModificationException( 309 "Skipping modify table operation on table '" + getTableName().toString() 310 + "' as it has already been modified by some other concurrent operation, " 311 + "Please retry."); 312 } 313 } else { 314 this.unmodifiedTableDescriptor = 315 env.getMasterServices().getTableDescriptors().get(getTableName()); 316 } 317 318 this.deleteColumnFamilyInModify = isDeleteColumnFamily(unmodifiedTableDescriptor, 319 modifiedTableDescriptor); 320 } 321 322 /** 323 * Find out whether all column families in unmodifiedTableDescriptor also exists in 324 * the modifiedTableDescriptor. 325 * @return True if we are deleting a column family. 326 */ 327 private static boolean isDeleteColumnFamily(TableDescriptor originalDescriptor, 328 TableDescriptor newDescriptor) { 329 boolean result = false; 330 final Set<byte[]> originalFamilies = originalDescriptor.getColumnFamilyNames(); 331 final Set<byte[]> newFamilies = newDescriptor.getColumnFamilyNames(); 332 for (byte[] familyName : originalFamilies) { 333 if (!newFamilies.contains(familyName)) { 334 result = true; 335 break; 336 } 337 } 338 return result; 339 } 340 341 /** 342 * Action before modifying table. 343 * @param env MasterProcedureEnv 344 * @param state the procedure state 345 */ 346 private void preModify(final MasterProcedureEnv env, final ModifyTableState state) 347 throws IOException, InterruptedException { 348 runCoprocessorAction(env, state); 349 } 350 351 /** 352 * Update descriptor 353 * @param env MasterProcedureEnv 354 **/ 355 private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException { 356 env.getMasterServices().getTableDescriptors().update(modifiedTableDescriptor); 357 } 358 359 /** 360 * Removes from hdfs the families that are not longer present in the new table descriptor. 361 * @param env MasterProcedureEnv 362 */ 363 private void deleteFromFs(final MasterProcedureEnv env, 364 final TableDescriptor oldTableDescriptor, final TableDescriptor newTableDescriptor) 365 throws IOException { 366 final Set<byte[]> oldFamilies = oldTableDescriptor.getColumnFamilyNames(); 367 final Set<byte[]> newFamilies = newTableDescriptor.getColumnFamilyNames(); 368 for (byte[] familyName : oldFamilies) { 369 if (!newFamilies.contains(familyName)) { 370 MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem( 371 env, 372 getTableName(), 373 getRegionInfoList(env), 374 familyName, 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}