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.assignment; 019 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.List; 026import java.util.stream.Stream; 027import org.apache.hadoop.conf.Configuration; 028import org.apache.hadoop.fs.FileSystem; 029import org.apache.hadoop.fs.Path; 030import org.apache.hadoop.hbase.MetaMutationAnnotation; 031import org.apache.hadoop.hbase.ServerName; 032import org.apache.hadoop.hbase.TableName; 033import org.apache.hadoop.hbase.UnknownRegionException; 034import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 035import org.apache.hadoop.hbase.client.DoNotRetryRegionException; 036import org.apache.hadoop.hbase.client.MasterSwitchType; 037import org.apache.hadoop.hbase.client.Mutation; 038import org.apache.hadoop.hbase.client.RegionInfo; 039import org.apache.hadoop.hbase.client.RegionInfoBuilder; 040import org.apache.hadoop.hbase.client.TableDescriptor; 041import org.apache.hadoop.hbase.exceptions.MergeRegionException; 042import org.apache.hadoop.hbase.io.hfile.CacheConfig; 043import org.apache.hadoop.hbase.master.MasterCoprocessorHost; 044import org.apache.hadoop.hbase.master.MasterFileSystem; 045import org.apache.hadoop.hbase.master.RegionState; 046import org.apache.hadoop.hbase.master.RegionState.State; 047import org.apache.hadoop.hbase.master.normalizer.NormalizationPlan; 048import org.apache.hadoop.hbase.master.procedure.AbstractStateMachineTableProcedure; 049import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv; 050import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil; 051import org.apache.hadoop.hbase.procedure2.ProcedureMetrics; 052import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer; 053import org.apache.hadoop.hbase.quotas.MasterQuotaManager; 054import org.apache.hadoop.hbase.quotas.QuotaExceededException; 055import org.apache.hadoop.hbase.regionserver.HRegionFileSystem; 056import org.apache.hadoop.hbase.regionserver.HStoreFile; 057import org.apache.hadoop.hbase.regionserver.StoreFileInfo; 058import org.apache.hadoop.hbase.regionserver.StoreUtils; 059import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTracker; 060import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory; 061import org.apache.hadoop.hbase.util.Bytes; 062import org.apache.hadoop.hbase.util.CommonFSUtils; 063import org.apache.hadoop.hbase.wal.WALSplitUtil; 064import org.apache.yetus.audience.InterfaceAudience; 065import org.slf4j.Logger; 066import org.slf4j.LoggerFactory; 067 068import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; 069import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionInfoResponse; 070import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos; 071import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.MergeTableRegionsState; 072 073/** 074 * The procedure to Merge regions in a table. This procedure takes an exclusive table lock since it 075 * is working over multiple regions. It holds the lock for the life of the procedure. Throws 076 * exception on construction if determines context hostile to merge (cluster going down or master is 077 * shutting down or table is disabled). 078 */ 079@InterfaceAudience.Private 080public class MergeTableRegionsProcedure 081 extends AbstractStateMachineTableProcedure<MergeTableRegionsState> { 082 private static final Logger LOG = LoggerFactory.getLogger(MergeTableRegionsProcedure.class); 083 private ServerName regionLocation; 084 085 /** 086 * Two or more regions to merge, the 'merge parents'. 087 */ 088 private RegionInfo[] regionsToMerge; 089 090 /** 091 * The resulting merged region. 092 */ 093 private RegionInfo mergedRegion; 094 095 private boolean force; 096 097 public MergeTableRegionsProcedure() { 098 // Required by the Procedure framework to create the procedure on replay 099 } 100 101 public MergeTableRegionsProcedure(final MasterProcedureEnv env, final RegionInfo[] regionsToMerge, 102 final boolean force) throws IOException { 103 super(env); 104 // Check parent regions. Make sure valid before starting work. 105 // This check calls the super method #checkOnline also. 106 checkRegionsToMerge(env, regionsToMerge, force); 107 // Sort the regions going into the merge. 108 Arrays.sort(regionsToMerge); 109 this.regionsToMerge = regionsToMerge; 110 this.mergedRegion = createMergedRegionInfo(regionsToMerge); 111 // Preflight depends on mergedRegion being set (at least). 112 preflightChecks(env, true); 113 this.force = force; 114 } 115 116 /** 117 * @throws MergeRegionException If unable to merge regions for whatever reasons. 118 */ 119 private static void checkRegionsToMerge(MasterProcedureEnv env, final RegionInfo[] regions, 120 final boolean force) throws MergeRegionException { 121 long count = Arrays.stream(regions).distinct().count(); 122 if (regions.length != count) { 123 throw new MergeRegionException("Duplicate regions specified; cannot merge a region to " 124 + "itself. Passed in " + regions.length + " but only " + count + " unique."); 125 } 126 if (count < 2) { 127 throw new MergeRegionException("Need two Regions at least to run a Merge"); 128 } 129 RegionInfo previous = null; 130 for (RegionInfo ri : regions) { 131 if (previous != null) { 132 if (!previous.getTable().equals(ri.getTable())) { 133 String msg = "Can't merge regions from different tables: " + previous + ", " + ri; 134 LOG.warn(msg); 135 throw new MergeRegionException(msg); 136 } 137 if (!force && !ri.isAdjacent(previous) && !ri.isOverlap(previous)) { 138 String msg = "Unable to merge non-adjacent or non-overlapping regions '" 139 + previous.getShortNameToLog() + "', '" + ri.getShortNameToLog() + "' when force=false"; 140 LOG.warn(msg); 141 throw new MergeRegionException(msg); 142 } 143 } 144 145 if (ri.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) { 146 throw new MergeRegionException("Can't merge non-default replicas; " + ri); 147 } 148 try { 149 checkOnline(env, ri); 150 } catch (DoNotRetryRegionException dnrre) { 151 throw new MergeRegionException(dnrre); 152 } 153 154 previous = ri; 155 } 156 } 157 158 /** 159 * Create merged region info by looking at passed in <code>regionsToMerge</code> to figure what 160 * extremes for start and end keys to use; merged region needs to have an extent sufficient to 161 * cover all regions-to-merge. 162 */ 163 private static RegionInfo createMergedRegionInfo(final RegionInfo[] regionsToMerge) { 164 byte[] lowestStartKey = null; 165 byte[] highestEndKey = null; 166 // Region Id is a timestamp. Merged region's id can't be less than that of 167 // merging regions else will insert at wrong location in hbase:meta (See HBASE-710). 168 long highestRegionId = -1; 169 for (RegionInfo ri : regionsToMerge) { 170 if (lowestStartKey == null) { 171 lowestStartKey = ri.getStartKey(); 172 } else if (Bytes.compareTo(ri.getStartKey(), lowestStartKey) < 0) { 173 lowestStartKey = ri.getStartKey(); 174 } 175 if (highestEndKey == null) { 176 highestEndKey = ri.getEndKey(); 177 } else if (ri.isLast() || Bytes.compareTo(ri.getEndKey(), highestEndKey) > 0) { 178 highestEndKey = ri.getEndKey(); 179 } 180 highestRegionId = ri.getRegionId() > highestRegionId ? ri.getRegionId() : highestRegionId; 181 } 182 // Merged region is sorted between two merging regions in META 183 return RegionInfoBuilder.newBuilder(regionsToMerge[0].getTable()).setStartKey(lowestStartKey) 184 .setEndKey(highestEndKey).setSplit(false) 185 .setRegionId(highestRegionId + 1/* Add one so new merged region is highest */).build(); 186 } 187 188 @Override 189 protected Flow executeFromState(final MasterProcedureEnv env, MergeTableRegionsState state) { 190 LOG.trace("{} execute state={}", this, state); 191 try { 192 switch (state) { 193 case MERGE_TABLE_REGIONS_PREPARE: 194 if (!prepareMergeRegion(env)) { 195 assert isFailed() : "Merge region should have an exception here"; 196 return Flow.NO_MORE_STATE; 197 } 198 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION); 199 break; 200 case MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION: 201 preMergeRegions(env); 202 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CLOSE_REGIONS); 203 break; 204 case MERGE_TABLE_REGIONS_CLOSE_REGIONS: 205 addChildProcedure(createUnassignProcedures(env)); 206 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS); 207 break; 208 case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS: 209 checkClosedRegions(env); 210 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CREATE_MERGED_REGION); 211 break; 212 case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION: 213 removeNonDefaultReplicas(env); 214 createMergedRegion(env); 215 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE); 216 break; 217 case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE: 218 writeMaxSequenceIdFile(env); 219 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION); 220 break; 221 case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION: 222 preMergeRegionsCommit(env); 223 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_UPDATE_META); 224 break; 225 case MERGE_TABLE_REGIONS_UPDATE_META: 226 updateMetaForMergedRegions(env); 227 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION); 228 break; 229 case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION: 230 postMergeRegionsCommit(env); 231 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_OPEN_MERGED_REGION); 232 break; 233 case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION: 234 addChildProcedure(createAssignProcedures(env)); 235 setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_OPERATION); 236 break; 237 case MERGE_TABLE_REGIONS_POST_OPERATION: 238 postCompletedMergeRegions(env); 239 return Flow.NO_MORE_STATE; 240 default: 241 throw new UnsupportedOperationException(this + " unhandled state=" + state); 242 } 243 } catch (IOException e) { 244 String msg = "Error trying to merge " + RegionInfo.getShortNameToLog(regionsToMerge) + " in " 245 + getTableName() + " (in state=" + state + ")"; 246 if (!isRollbackSupported(state)) { 247 // We reach a state that cannot be rolled back. We just need to keep retrying. 248 LOG.warn(msg, e); 249 } else { 250 LOG.error(msg, e); 251 setFailure("master-merge-regions", e); 252 } 253 } 254 return Flow.HAS_MORE_STATE; 255 } 256 257 /** 258 * To rollback {@link MergeTableRegionsProcedure}, two AssignProcedures are asynchronously 259 * submitted for each region to be merged (rollback doesn't wait on the completion of the 260 * AssignProcedures) . This can be improved by changing rollback() to support sub-procedures. See 261 * HBASE-19851 for details. 262 */ 263 @Override 264 protected void rollbackState(final MasterProcedureEnv env, final MergeTableRegionsState state) 265 throws IOException { 266 LOG.trace("{} rollback state={}", this, state); 267 268 try { 269 switch (state) { 270 case MERGE_TABLE_REGIONS_POST_OPERATION: 271 case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION: 272 case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION: 273 case MERGE_TABLE_REGIONS_UPDATE_META: 274 String msg = this + " We are in the " + state + " state." 275 + " It is complicated to rollback the merge operation that region server is working on." 276 + " Rollback is not supported and we should let the merge operation to complete"; 277 LOG.warn(msg); 278 // PONR 279 throw new UnsupportedOperationException(this + " unhandled state=" + state); 280 case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION: 281 break; 282 case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION: 283 case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE: 284 cleanupMergedRegion(env); 285 break; 286 case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS: 287 rollbackCloseRegionsForMerge(env); 288 break; 289 case MERGE_TABLE_REGIONS_CLOSE_REGIONS: 290 // No-op. The reopen TransitRegionStateProcedures for the two parents were already 291 // submitted by the rollback of MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS above; 292 // submitting a second batch here would race the first. Mirrors 293 // SplitTableRegionProcedure's rollback of SPLIT_TABLE_REGION_CLOSE_PARENT_REGION. 294 break; 295 case MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION: 296 postRollBackMergeRegions(env); 297 break; 298 case MERGE_TABLE_REGIONS_PREPARE: 299 rollbackPrepareMerge(env); 300 break; 301 default: 302 throw new UnsupportedOperationException(this + " unhandled state=" + state); 303 } 304 } catch (Exception e) { 305 // This will be retried. Unless there is a bug in the code, 306 // this should be just a "temporary error" (e.g. network down) 307 LOG.warn("Failed rollback attempt step " + state + " for merging the regions " 308 + RegionInfo.getShortNameToLog(regionsToMerge) + " in table " + getTableName(), e); 309 throw e; 310 } 311 } 312 313 /* 314 * Check whether we are in the state that can be rolled back 315 */ 316 @Override 317 protected boolean isRollbackSupported(final MergeTableRegionsState state) { 318 switch (state) { 319 case MERGE_TABLE_REGIONS_POST_OPERATION: 320 case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION: 321 case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION: 322 case MERGE_TABLE_REGIONS_UPDATE_META: 323 // It is not safe to rollback in these states. 324 return false; 325 default: 326 break; 327 } 328 return true; 329 } 330 331 private void removeNonDefaultReplicas(MasterProcedureEnv env) throws IOException { 332 AssignmentManagerUtil.removeNonDefaultReplicas(env, Stream.of(regionsToMerge), 333 getRegionReplication(env)); 334 } 335 336 private void checkClosedRegions(MasterProcedureEnv env) throws IOException { 337 // Theoretically this should not happen any more after we use TRSP, but anyway 338 // let's add a check here 339 for (RegionInfo region : regionsToMerge) { 340 AssignmentManagerUtil.checkClosedRegion(env, region); 341 } 342 } 343 344 @Override 345 protected MergeTableRegionsState getState(final int stateId) { 346 return MergeTableRegionsState.forNumber(stateId); 347 } 348 349 @Override 350 protected int getStateId(final MergeTableRegionsState state) { 351 return state.getNumber(); 352 } 353 354 @Override 355 protected MergeTableRegionsState getInitialState() { 356 return MergeTableRegionsState.MERGE_TABLE_REGIONS_PREPARE; 357 } 358 359 @Override 360 protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { 361 super.serializeStateData(serializer); 362 363 final MasterProcedureProtos.MergeTableRegionsStateData.Builder mergeTableRegionsMsg = 364 MasterProcedureProtos.MergeTableRegionsStateData.newBuilder() 365 .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser())) 366 .setMergedRegionInfo(ProtobufUtil.toRegionInfo(mergedRegion)).setForcible(force); 367 for (RegionInfo ri : regionsToMerge) { 368 mergeTableRegionsMsg.addRegionInfo(ProtobufUtil.toRegionInfo(ri)); 369 } 370 serializer.serialize(mergeTableRegionsMsg.build()); 371 } 372 373 @Override 374 protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { 375 super.deserializeStateData(serializer); 376 377 final MasterProcedureProtos.MergeTableRegionsStateData mergeTableRegionsMsg = 378 serializer.deserialize(MasterProcedureProtos.MergeTableRegionsStateData.class); 379 setUser(MasterProcedureUtil.toUserInfo(mergeTableRegionsMsg.getUserInfo())); 380 381 assert (mergeTableRegionsMsg.getRegionInfoCount() == 2); 382 regionsToMerge = new RegionInfo[mergeTableRegionsMsg.getRegionInfoCount()]; 383 for (int i = 0; i < regionsToMerge.length; i++) { 384 regionsToMerge[i] = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getRegionInfo(i)); 385 } 386 387 mergedRegion = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getMergedRegionInfo()); 388 } 389 390 @Override 391 public void toStringClassDetails(StringBuilder sb) { 392 sb.append(getClass().getSimpleName()); 393 sb.append(" table="); 394 sb.append(getTableName()); 395 sb.append(", regions="); 396 sb.append(RegionInfo.getShortNameToLog(regionsToMerge)); 397 sb.append(", force="); 398 sb.append(force); 399 } 400 401 @Override 402 protected LockState acquireLock(final MasterProcedureEnv env) { 403 RegionInfo[] lockRegions = Arrays.copyOf(regionsToMerge, regionsToMerge.length + 1); 404 lockRegions[lockRegions.length - 1] = mergedRegion; 405 406 if (env.getProcedureScheduler().waitRegions(this, getTableName(), lockRegions)) { 407 try { 408 LOG.debug(LockState.LOCK_EVENT_WAIT + " " + env.getProcedureScheduler().dumpLocks()); 409 } catch (IOException e) { 410 // Ignore, just for logging 411 } 412 return LockState.LOCK_EVENT_WAIT; 413 } 414 return LockState.LOCK_ACQUIRED; 415 } 416 417 @Override 418 protected void releaseLock(final MasterProcedureEnv env) { 419 RegionInfo[] lockRegions = Arrays.copyOf(regionsToMerge, regionsToMerge.length + 1); 420 lockRegions[lockRegions.length - 1] = mergedRegion; 421 422 env.getProcedureScheduler().wakeRegions(this, getTableName(), lockRegions); 423 } 424 425 @Override 426 protected boolean holdLock(MasterProcedureEnv env) { 427 return true; 428 } 429 430 @Override 431 public TableName getTableName() { 432 return mergedRegion.getTable(); 433 } 434 435 @Override 436 public TableOperationType getTableOperationType() { 437 return TableOperationType.REGION_MERGE; 438 } 439 440 @Override 441 protected ProcedureMetrics getProcedureMetrics(MasterProcedureEnv env) { 442 return env.getAssignmentManager().getAssignmentManagerMetrics().getMergeProcMetrics(); 443 } 444 445 /** 446 * Prepare merge and do some check 447 */ 448 private boolean prepareMergeRegion(final MasterProcedureEnv env) throws IOException { 449 // Fail if we are taking snapshot for the given table 450 TableName tn = regionsToMerge[0].getTable(); 451 final String regionNamesToLog = RegionInfo.getShortNameToLog(regionsToMerge); 452 if (env.getMasterServices().getSnapshotManager().isTableTakingAnySnapshot(tn)) { 453 throw new MergeRegionException( 454 "Skip merging regions " + regionNamesToLog + ", because we are snapshotting " + tn); 455 } 456 457 /* 458 * Sometimes a ModifyTableProcedure has edited a table descriptor to change the number of region 459 * replicas for a table, but it has not yet opened/closed the new replicas. The 460 * ModifyTableProcedure assumes that nobody else will do the opening/closing of the new 461 * replicas, but a concurrent MergeTableRegionProcedure would violate that assumption. 462 */ 463 if (isTableModificationInProgress(env)) { 464 setFailure(getClass().getSimpleName(), 465 new IOException("Skip merging regions " + regionNamesToLog 466 + ", because there is an active procedure that is modifying the table " + tn)); 467 return false; 468 } 469 470 // Mostly the below two checks are not used because we already check the switches before 471 // submitting the merge procedure. Just for safety, we are checking the switch again here. 472 // Also, in case the switch was set to false after submission, this procedure can be rollbacked, 473 // thanks to this double check! 474 // case 1: check for cluster level switch 475 if (!env.getMasterServices().isSplitOrMergeEnabled(MasterSwitchType.MERGE)) { 476 LOG.warn("Merge switch is off! skip merge of " + regionNamesToLog); 477 setFailure(getClass().getSimpleName(), 478 new IOException("Merge of " + regionNamesToLog + " failed because merge switch is off")); 479 return false; 480 } 481 // case 2: check for table level switch 482 if (!env.getMasterServices().getTableDescriptors().get(getTableName()).isMergeEnabled()) { 483 LOG.warn("Merge is disabled for the table! Skipping merge of {}", regionNamesToLog); 484 setFailure(getClass().getSimpleName(), new IOException( 485 "Merge of " + regionNamesToLog + " failed as region merge is disabled for the table")); 486 return false; 487 } 488 489 RegionStates regionStates = env.getAssignmentManager().getRegionStates(); 490 RegionStateStore regionStateStore = env.getAssignmentManager().getRegionStateStore(); 491 for (RegionInfo ri : this.regionsToMerge) { 492 if (regionStateStore.hasMergeRegions(ri)) { 493 String msg = "Skip merging " + regionNamesToLog + ", because a parent, " 494 + RegionInfo.getShortNameToLog(ri) + ", has a merge qualifier " 495 + "(if a 'merge column' in parent, it was recently merged but still has outstanding " 496 + "references to its parents that must be cleared before it can participate in merge -- " 497 + "major compact it to hurry clearing of its references)"; 498 LOG.warn(msg); 499 throw new MergeRegionException(msg); 500 } 501 RegionState state = regionStates.getRegionState(ri.getEncodedName()); 502 if (state == null) { 503 throw new UnknownRegionException( 504 RegionInfo.getShortNameToLog(ri) + " UNKNOWN (Has it been garbage collected?)"); 505 } 506 if (!state.isOpened()) { 507 throw new MergeRegionException("Unable to merge regions that are NOT online: " + ri); 508 } 509 // Ask the remote regionserver if regions are mergeable. If we get an IOE, report it 510 // along with the failure, so we can see why regions are not mergeable at this time. 511 try { 512 if (!isMergeable(env, state)) { 513 setFailure(getClass().getSimpleName(), 514 new MergeRegionException("Skip merging " + regionNamesToLog + ", because a parent, " 515 + RegionInfo.getShortNameToLog(ri) + ", is not mergeable")); 516 return false; 517 } 518 } catch (IOException e) { 519 IOException ioe = new IOException(RegionInfo.getShortNameToLog(ri) + " NOT mergeable", e); 520 setFailure(getClass().getSimpleName(), ioe); 521 return false; 522 } 523 } 524 525 // Update region states to Merging 526 setRegionStateToMerging(env); 527 return true; 528 } 529 530 private boolean isMergeable(final MasterProcedureEnv env, final RegionState rs) 531 throws IOException { 532 GetRegionInfoResponse response = 533 AssignmentManagerUtil.getRegionInfoResponse(env, rs.getServerName(), rs.getRegion()); 534 return response.hasMergeable() && response.getMergeable(); 535 } 536 537 /** 538 * Action for rollback a merge table after prepare merge 539 */ 540 private void rollbackPrepareMerge(final MasterProcedureEnv env) throws IOException { 541 for (RegionInfo rinfo : regionsToMerge) { 542 RegionStateNode regionStateNode = 543 env.getAssignmentManager().getRegionStates().getRegionStateNode(rinfo); 544 if (regionStateNode.getState() == State.MERGING) { 545 regionStateNode.setState(State.OPEN); 546 } 547 } 548 } 549 550 /** 551 * Pre merge region action 552 * @param env MasterProcedureEnv 553 **/ 554 private void preMergeRegions(final MasterProcedureEnv env) throws IOException { 555 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 556 if (cpHost != null) { 557 cpHost.preMergeRegionsAction(regionsToMerge, getUser()); 558 } 559 // TODO: Clean up split and merge. Currently all over the place. 560 try { 561 MasterQuotaManager masterQuotaManager = env.getMasterServices().getMasterQuotaManager(); 562 if (masterQuotaManager != null) { 563 masterQuotaManager.onRegionMerged(this.mergedRegion); 564 } 565 } catch (QuotaExceededException e) { 566 // TODO: why is this here? merge requests can be submitted by actors other than the normalizer 567 env.getMasterServices().getRegionNormalizerManager() 568 .planSkipped(NormalizationPlan.PlanType.MERGE); 569 throw e; 570 } 571 } 572 573 /** 574 * Action after rollback a merge table regions action. 575 */ 576 private void postRollBackMergeRegions(final MasterProcedureEnv env) throws IOException { 577 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 578 if (cpHost != null) { 579 cpHost.postRollBackMergeRegionsAction(regionsToMerge, getUser()); 580 } 581 } 582 583 /** 584 * Set the region states to MERGING state 585 */ 586 private void setRegionStateToMerging(final MasterProcedureEnv env) { 587 // Set State.MERGING to regions to be merged 588 RegionStates regionStates = env.getAssignmentManager().getRegionStates(); 589 for (RegionInfo ri : this.regionsToMerge) { 590 regionStates.getRegionStateNode(ri).setState(State.MERGING); 591 } 592 } 593 594 /** 595 * Create merged region. The way the merge works is that we make a 'merges' temporary directory in 596 * the FIRST parent region to merge (Do not change this without also changing the rollback where 597 * we look in this FIRST region for the merge dir). We then collect here references to all the 598 * store files in all the parent regions including those of the FIRST parent region into a 599 * subdirectory, named for the resultant merged region. We then call commitMergeRegion. It finds 600 * this subdirectory of storefile references and moves them under the new merge region (creating 601 * the region layout as side effect). After assign of the new merge region, we will run a 602 * compaction. This will undo the references but the reference files remain in place until the 603 * archiver runs (which it does on a period as a chore in the RegionServer that hosts the merge 604 * region -- see CompactedHFilesDischarger). Once the archiver has moved aside the no-longer used 605 * references, the merge region no longer has references. The catalog janitor will notice when it 606 * runs next and it will remove the old parent regions. 607 */ 608 private void createMergedRegion(final MasterProcedureEnv env) throws IOException { 609 final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem(); 610 final Path tableDir = CommonFSUtils.getTableDir(mfs.getRootDir(), regionsToMerge[0].getTable()); 611 final FileSystem fs = mfs.getFileSystem(); 612 List<StoreFileInfo> mergedFiles = new ArrayList<StoreFileInfo>(); 613 HRegionFileSystem mergeRegionFs = HRegionFileSystem 614 .createRegionOnFileSystem(env.getMasterConfiguration(), fs, tableDir, mergedRegion); 615 616 for (RegionInfo ri : this.regionsToMerge) { 617 HRegionFileSystem regionFs = HRegionFileSystem 618 .openRegionFromFileSystem(env.getMasterConfiguration(), fs, tableDir, ri, false); 619 mergedFiles.addAll(mergeStoreFiles(env, regionFs, mergeRegionFs, mergedRegion)); 620 } 621 assert mergeRegionFs != null; 622 mergeRegionFs.commitMergedRegion(mergedFiles, env); 623 624 // Prepare to create merged regions 625 env.getAssignmentManager().getRegionStates().getOrCreateRegionStateNode(mergedRegion) 626 .setState(State.MERGING_NEW); 627 } 628 629 private List<StoreFileInfo> mergeStoreFiles(MasterProcedureEnv env, HRegionFileSystem regionFs, 630 HRegionFileSystem mergeRegionFs, RegionInfo mergedRegion) throws IOException { 631 final TableDescriptor htd = 632 env.getMasterServices().getTableDescriptors().get(mergedRegion.getTable()); 633 List<StoreFileInfo> mergedFiles = new ArrayList<StoreFileInfo>(); 634 for (ColumnFamilyDescriptor hcd : htd.getColumnFamilies()) { 635 String family = hcd.getNameAsString(); 636 StoreFileTracker tracker = 637 StoreFileTrackerFactory.create(env.getMasterConfiguration(), htd, hcd, regionFs); 638 final Collection<StoreFileInfo> storeFiles = tracker.load(); 639 if (storeFiles != null && storeFiles.size() > 0) { 640 final Configuration storeConfiguration = 641 StoreUtils.createStoreConfiguration(env.getMasterConfiguration(), htd, hcd); 642 for (StoreFileInfo storeFileInfo : storeFiles) { 643 // Create reference file(s) to parent region file here in mergedDir. 644 // As this procedure is running on master, use CacheConfig.DISABLED means 645 // don't cache any block. 646 // We also need to pass through a suitable CompoundConfiguration as if this 647 // is running in a regionserver's Store context, or we might not be able 648 // to read the hfiles. 649 storeFileInfo.setConf(storeConfiguration); 650 StoreFileInfo refFile = mergeRegionFs.mergeStoreFile(regionFs.getRegionInfo(), family, 651 new HStoreFile(storeFileInfo, hcd.getBloomFilterType(), CacheConfig.DISABLED), tracker); 652 mergedFiles.add(refFile); 653 } 654 } 655 } 656 return mergedFiles; 657 } 658 659 /** 660 * Clean up a merged region on rollback after failure. 661 */ 662 private void cleanupMergedRegion(final MasterProcedureEnv env) throws IOException { 663 final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem(); 664 TableName tn = this.regionsToMerge[0].getTable(); 665 final Path tabledir = CommonFSUtils.getTableDir(mfs.getRootDir(), tn); 666 final FileSystem fs = mfs.getFileSystem(); 667 // See createMergedRegion above where we specify the merge dir as being in the 668 // FIRST merge parent region. 669 HRegionFileSystem regionFs = HRegionFileSystem.openRegionFromFileSystem( 670 env.getMasterConfiguration(), fs, tabledir, regionsToMerge[0], false); 671 regionFs.cleanupMergedRegion(mergedRegion); 672 } 673 674 /** 675 * Rollback close regions 676 **/ 677 private void rollbackCloseRegionsForMerge(MasterProcedureEnv env) throws IOException { 678 // At this point we should check if region was actually closed. If it was not closed then we 679 // don't need to reopen the region and we can just change the regionNode state to OPEN. 680 // if it is already closed then we need to do a reopen of region 681 List<RegionInfo> toAssign = new ArrayList<>(); 682 for (RegionInfo rinfo : regionsToMerge) { 683 RegionStateNode regionStateNode = 684 env.getAssignmentManager().getRegionStates().getRegionStateNode(rinfo); 685 if (regionStateNode.getState() != State.MERGING) { 686 // same as before HBASE-28405 687 toAssign.add(rinfo); 688 } 689 } 690 AssignmentManagerUtil.reopenRegionsForRollback(env, toAssign, getRegionReplication(env), 691 getServerName(env)); 692 } 693 694 private TransitRegionStateProcedure[] createUnassignProcedures(MasterProcedureEnv env) 695 throws IOException { 696 return AssignmentManagerUtil.createUnassignProceduresForSplitOrMerge(env, 697 Stream.of(regionsToMerge), getRegionReplication(env)); 698 } 699 700 private TransitRegionStateProcedure[] createAssignProcedures(MasterProcedureEnv env) 701 throws IOException { 702 return AssignmentManagerUtil.createAssignProceduresForOpeningNewRegions(env, 703 Collections.singletonList(mergedRegion), getRegionReplication(env), getServerName(env)); 704 } 705 706 private int getRegionReplication(final MasterProcedureEnv env) throws IOException { 707 return env.getMasterServices().getTableDescriptors().get(getTableName()).getRegionReplication(); 708 } 709 710 /** 711 * Post merge region action 712 * @param env MasterProcedureEnv 713 **/ 714 private void preMergeRegionsCommit(final MasterProcedureEnv env) throws IOException { 715 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 716 if (cpHost != null) { 717 @MetaMutationAnnotation 718 final List<Mutation> metaEntries = new ArrayList<>(); 719 cpHost.preMergeRegionsCommit(regionsToMerge, metaEntries, getUser()); 720 try { 721 for (Mutation p : metaEntries) { 722 RegionInfo.parseRegionName(p.getRow()); 723 } 724 } catch (IOException e) { 725 LOG.error( 726 "Row key of mutation from coprocessor is not parsable as region name. " 727 + "Mutations from coprocessor should only be for {} table.", 728 TableName.META_TABLE_NAME, e); 729 throw e; 730 } 731 } 732 } 733 734 /** 735 * Add merged region to META and delete original regions. 736 */ 737 private void updateMetaForMergedRegions(final MasterProcedureEnv env) throws IOException { 738 env.getAssignmentManager().markRegionAsMerged(mergedRegion, getServerName(env), 739 this.regionsToMerge); 740 } 741 742 /** 743 * Post merge region action 744 * @param env MasterProcedureEnv 745 **/ 746 private void postMergeRegionsCommit(final MasterProcedureEnv env) throws IOException { 747 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 748 if (cpHost != null) { 749 cpHost.postMergeRegionsCommit(regionsToMerge, mergedRegion, getUser()); 750 } 751 } 752 753 /** 754 * Post merge region action 755 * @param env MasterProcedureEnv 756 **/ 757 private void postCompletedMergeRegions(final MasterProcedureEnv env) throws IOException { 758 final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); 759 if (cpHost != null) { 760 cpHost.postCompletedMergeRegionsAction(regionsToMerge, mergedRegion, getUser()); 761 } 762 } 763 764 /** 765 * The procedure could be restarted from a different machine. If the variable is null, we need to 766 * retrieve it. 767 * @param env MasterProcedureEnv 768 */ 769 private ServerName getServerName(final MasterProcedureEnv env) { 770 if (regionLocation == null) { 771 regionLocation = 772 env.getAssignmentManager().getRegionStates().getRegionServerOfRegion(regionsToMerge[0]); 773 // May still be null here but return null and let caller deal. 774 // Means we lost the in-memory-only location. We are in recovery 775 // or so. The caller should be able to deal w/ a null ServerName. 776 // Let them go to the Balancer to find one to use instead. 777 } 778 return regionLocation; 779 } 780 781 private void writeMaxSequenceIdFile(MasterProcedureEnv env) throws IOException { 782 MasterFileSystem fs = env.getMasterFileSystem(); 783 long maxSequenceId = -1L; 784 for (RegionInfo region : regionsToMerge) { 785 maxSequenceId = 786 Math.max(maxSequenceId, WALSplitUtil.getMaxRegionSequenceId(env.getMasterConfiguration(), 787 region, fs::getFileSystem, fs::getWALFileSystem)); 788 } 789 if (maxSequenceId > 0) { 790 WALSplitUtil.writeRegionSequenceIdFile(fs.getWALFileSystem(), 791 getWALRegionDir(env, mergedRegion), maxSequenceId); 792 } 793 } 794 795 /** Returns The merged region. Maybe be null if called to early or we failed. */ 796 RegionInfo getMergedRegion() { 797 return this.mergedRegion; 798 } 799 800 @Override 801 protected boolean abort(MasterProcedureEnv env) { 802 // Abort means rollback. We can't rollback all steps. HBASE-18018 added abort to all 803 // Procedures. Here is a Procedure that has a PONR and cannot be aborted once it enters this 804 // range of steps; what do we do for these should an operator want to cancel them? HBASE-20022. 805 return isRollbackSupported(getCurrentState()) && super.abort(env); 806 } 807}