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 static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import java.util.concurrent.atomic.AtomicInteger;
025import java.util.concurrent.atomic.AtomicLong;
026import org.apache.hadoop.hbase.client.RegionInfo;
027import org.apache.hadoop.hbase.client.RegionInfoBuilder;
028import org.apache.hadoop.hbase.master.RegionState;
029import org.apache.hadoop.hbase.testclassification.MasterTests;
030import org.apache.hadoop.hbase.testclassification.SmallTests;
031import org.apache.hadoop.hbase.util.EnvironmentEdgeManagerTestHelper;
032import org.apache.hadoop.hbase.util.ManualEnvironmentEdge;
033import org.junit.jupiter.api.AfterEach;
034import org.junit.jupiter.api.BeforeEach;
035import org.junit.jupiter.api.Tag;
036import org.junit.jupiter.api.Test;
037
038@Tag(MasterTests.TAG)
039@Tag(SmallTests.TAG)
040public class TestRegionInTransitionTracker {
041
042  private RegionInTransitionTracker tracker;
043  private RegionInfo regionInfo;
044  private RegionStateNode regionStateNode;
045  private ManualEnvironmentEdge edge;
046  private AtomicLong ritDuration;
047  private AtomicInteger ritDurationCalls;
048
049  @BeforeEach
050  public void setUp() {
051    ritDuration = new AtomicLong(-1L);
052    ritDurationCalls = new AtomicInteger();
053    tracker = new RegionInTransitionTracker(duration -> {
054      ritDuration.set(duration);
055      ritDurationCalls.incrementAndGet();
056    });
057    regionInfo = RegionInfoBuilder.FIRST_META_REGIONINFO;
058    regionStateNode = new RegionStateNode(regionInfo, new AtomicInteger());
059
060    edge = new ManualEnvironmentEdge();
061    edge.setValue(1_000L);
062    EnvironmentEdgeManagerTestHelper.injectEdge(edge);
063  }
064
065  @AfterEach
066  public void tearDown() {
067    EnvironmentEdgeManagerTestHelper.reset();
068    tracker.stop();
069  }
070
071  @Test
072  public void testInjectedRitDurationConsumerUsesFirstEnterTimestamp() throws Exception {
073    regionStateNode.setState(RegionState.State.OPEN);
074    tracker.handleRegionStateNodeOperation(regionStateNode);
075    assertFalse(tracker.isRegionInTransition(regionInfo));
076    assertFalse(tracker.hasRegionsInTransition());
077    assertEquals(0, tracker.getRegionsInTransition().size());
078
079    edge.incValue(100L);
080    regionStateNode.transitionState(RegionState.State.CLOSING, RegionState.State.OPEN);
081    tracker.handleRegionStateNodeOperation(regionStateNode);
082    assertTrue(tracker.isRegionInTransition(regionInfo));
083    assertEquals(1, tracker.getRegionsInTransition().size());
084
085    edge.incValue(100L);
086    regionStateNode.transitionState(RegionState.State.CLOSED, RegionState.State.CLOSING);
087    tracker.handleRegionStateNodeOperation(regionStateNode);
088    assertTrue(tracker.isRegionInTransition(regionInfo));
089    assertEquals(1, tracker.getRegionsInTransition().size());
090
091    edge.incValue(100L);
092    regionStateNode.transitionState(RegionState.State.OPENING, RegionState.State.CLOSED);
093    tracker.handleRegionStateNodeOperation(regionStateNode);
094    assertTrue(tracker.isRegionInTransition(regionInfo));
095    assertEquals(1, tracker.getRegionsInTransition().size());
096
097    edge.incValue(100L);
098    regionStateNode.transitionState(RegionState.State.OPEN, RegionState.State.OPENING);
099    tracker.handleRegionStateNodeOperation(regionStateNode);
100
101    assertFalse(tracker.isRegionInTransition(regionInfo));
102    assertFalse(tracker.hasRegionsInTransition());
103    assertEquals(1, ritDurationCalls.get());
104    assertTrue(ritDuration.get() >= 0);
105    assertEquals(300L, ritDuration.get());
106  }
107
108  @Test
109  public void testRegionCrashUsesCrashTimestampAsRitStart() {
110    regionStateNode.setState(RegionState.State.OPEN);
111
112    edge.incValue(100L);
113    long crashTime = edge.currentTime();
114    regionStateNode.crashed(crashTime);
115
116    edge.incValue(100L);
117    tracker.regionCrashed(regionStateNode, crashTime);
118    assertTrue(tracker.isRegionInTransition(regionInfo));
119
120    edge.incValue(200L);
121    tracker.handleRegionStateNodeOperation(regionStateNode);
122
123    assertFalse(tracker.isRegionInTransition(regionInfo));
124    assertEquals(1, ritDurationCalls.get());
125    assertEquals(300L, ritDuration.get());
126  }
127
128  @Test
129  public void testRegionCrashWithStaleProcedureUsesCrashTimestamp() {
130    // A procedure attached before the hosting server died reports an old timestamp via
131    // getLastUpdate(); the crash time must still win as the RIT start so the duration is not
132    // anchored at the stale procedure time.
133    long staleProcTime = 100L;
134    regionStateNode.setProcedure(new FixedLastUpdateProcedure(staleProcTime));
135    regionStateNode.setState(RegionState.State.OPEN);
136    // Sanity: getLastUpdate() is masked by the attached procedure.
137    assertEquals(staleProcTime, regionStateNode.getLastUpdate());
138
139    edge.incValue(100L);
140    long crashTime = edge.currentTime();
141    regionStateNode.crashed(crashTime);
142
143    edge.incValue(100L);
144    tracker.regionCrashed(regionStateNode, crashTime);
145    assertTrue(tracker.isRegionInTransition(regionInfo));
146
147    edge.incValue(200L);
148    tracker.handleRegionStateNodeOperation(regionStateNode);
149
150    assertFalse(tracker.isRegionInTransition(regionInfo));
151    assertEquals(1, ritDurationCalls.get());
152    // Anchored at crashTime (1100), not the stale procedure time (100): 1400 - 1100 = 300.
153    assertEquals(300L, ritDuration.get());
154  }
155
156  @Test
157  public void testProcedureLastUpdateUsedAsRitStartForNormalTransition() {
158    // For a normal (non-crash) transition the RIT start follows RegionStateNode#getLastUpdate,
159    // which is the attached procedure's last update time. This exercises the procedure-masked
160    // branch of getLastUpdate() that production always hits.
161    long procTime = 1200L;
162    regionStateNode.setProcedure(new FixedLastUpdateProcedure(procTime));
163    regionStateNode.setState(RegionState.State.OPENING);
164    tracker.handleRegionStateNodeOperation(regionStateNode);
165    assertTrue(tracker.isRegionInTransition(regionInfo));
166
167    edge.setValue(1500L);
168    regionStateNode.setState(RegionState.State.OPEN);
169    tracker.handleRegionStateNodeOperation(regionStateNode);
170
171    assertFalse(tracker.isRegionInTransition(regionInfo));
172    assertEquals(1, ritDurationCalls.get());
173    // Start anchored at the procedure lastUpdate (1200), removed at 1500 -> 300.
174    assertEquals(300L, ritDuration.get());
175  }
176
177  /**
178   * Minimal {@link TransitRegionStateProcedure} stub whose {@link #getLastUpdate()} returns a fixed
179   * value, so tests can exercise the procedure-masked branch of
180   * {@link RegionStateNode#getLastUpdate()} without standing up a procedure executor.
181   */
182  private static final class FixedLastUpdateProcedure extends TransitRegionStateProcedure {
183    private final long lastUpdate;
184
185    FixedLastUpdateProcedure(long lastUpdate) {
186      this.lastUpdate = lastUpdate;
187    }
188
189    @Override
190    public long getLastUpdate() {
191      return lastUpdate;
192    }
193  }
194}