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