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.regionserver.metrics;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertTrue;
022
023import java.util.Optional;
024import java.util.concurrent.CountDownLatch;
025import java.util.concurrent.ExecutorService;
026import java.util.concurrent.Executors;
027import java.util.concurrent.TimeUnit;
028import java.util.concurrent.atomic.AtomicInteger;
029import org.apache.hadoop.hbase.HBaseClassTestRule;
030import org.apache.hadoop.hbase.metrics.Counter;
031import org.apache.hadoop.hbase.metrics.Metric;
032import org.apache.hadoop.hbase.metrics.MetricRegistries;
033import org.apache.hadoop.hbase.metrics.MetricRegistry;
034import org.apache.hadoop.hbase.metrics.MetricRegistryInfo;
035import org.apache.hadoop.hbase.quotas.RpcThrottlingException;
036import org.apache.hadoop.hbase.testclassification.RegionServerTests;
037import org.apache.hadoop.hbase.testclassification.SmallTests;
038import org.junit.After;
039import org.junit.ClassRule;
040import org.junit.Test;
041import org.junit.experimental.categories.Category;
042
043@Category({ RegionServerTests.class, SmallTests.class })
044public class TestMetricsThrottleExceptions {
045
046  @ClassRule
047  public static final HBaseClassTestRule CLASS_RULE =
048    HBaseClassTestRule.forClass(TestMetricsThrottleExceptions.class);
049
050  private MetricRegistry testRegistry;
051  private MetricsThrottleExceptions throttleMetrics;
052
053  @After
054  public void cleanup() {
055    // Clean up global registries after each test to avoid interference
056    MetricRegistries.global().clear();
057  }
058
059  @Test
060  public void testBasicThrottleMetricsRecording() {
061    setupTestMetrics();
062
063    // Record a throttle exception
064    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded,
065      "alice", "users");
066
067    // Verify the counter exists and has correct value
068    Optional<Metric> metric =
069      testRegistry.get("RpcThrottlingException_Type_NumRequestsExceeded_User_alice_Table_users");
070    assertTrue("Counter metric should be present", metric.isPresent());
071    assertTrue("Metric should be a counter", metric.get() instanceof Counter);
072
073    Counter counter = (Counter) metric.get();
074    assertEquals("Counter should have count of 1", 1, counter.getCount());
075  }
076
077  @Test
078  public void testMultipleThrottleTypes() {
079    setupTestMetrics();
080
081    // Record different types of throttle exceptions
082    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded,
083      "alice", "users");
084    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.WriteSizeExceeded, "bob",
085      "logs");
086    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.ReadSizeExceeded, "charlie",
087      "metadata");
088
089    // Verify all three counters were created
090    verifyCounter(testRegistry,
091      "RpcThrottlingException_Type_NumRequestsExceeded_User_alice_Table_users", 1);
092    verifyCounter(testRegistry, "RpcThrottlingException_Type_WriteSizeExceeded_User_bob_Table_logs",
093      1);
094    verifyCounter(testRegistry,
095      "RpcThrottlingException_Type_ReadSizeExceeded_User_charlie_Table_metadata", 1);
096  }
097
098  @Test
099  public void testCounterIncrement() {
100    setupTestMetrics();
101
102    // Record the same throttle exception multiple times
103    String metricName = "RpcThrottlingException_Type_NumRequestsExceeded_User_alice_Table_users";
104    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded,
105      "alice", "users");
106    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded,
107      "alice", "users");
108    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded,
109      "alice", "users");
110
111    // Verify the counter incremented correctly
112    verifyCounter(testRegistry, metricName, 3);
113  }
114
115  @Test
116  public void testMetricNameSanitization() {
117    setupTestMetrics();
118
119    // Test that meaningful characters are preserved (hyphens, periods, etc.)
120    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.WriteSizeExceeded,
121      "user.name@company", "my-table-prod");
122
123    // Verify meaningful characters are preserved, only JMX-problematic chars are replaced
124    String expectedMetricName =
125      "RpcThrottlingException_Type_WriteSizeExceeded_User_user.name@company_Table_my-table-prod";
126    verifyCounter(testRegistry, expectedMetricName, 1);
127
128    // Test that JMX-problematic characters are sanitized
129    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.ReadSizeExceeded,
130      "user,with=bad:chars*", "table?with\"quotes");
131    String problematicMetricName =
132      "RpcThrottlingException_Type_ReadSizeExceeded_User_user_with_bad_chars__Table_table_with_quotes";
133    verifyCounter(testRegistry, problematicMetricName, 1);
134  }
135
136  @Test
137  public void testNullHandling() {
138    setupTestMetrics();
139
140    // Test null user and table names
141    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded, null,
142      null);
143    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.WriteSizeExceeded, "alice",
144      null);
145    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.ReadSizeExceeded, null,
146      "users");
147
148    // Verify null values are replaced with "unknown"
149    verifyCounter(testRegistry,
150      "RpcThrottlingException_Type_NumRequestsExceeded_User_unknown_Table_unknown", 1);
151    verifyCounter(testRegistry,
152      "RpcThrottlingException_Type_WriteSizeExceeded_User_alice_Table_unknown", 1);
153    verifyCounter(testRegistry,
154      "RpcThrottlingException_Type_ReadSizeExceeded_User_unknown_Table_users", 1);
155  }
156
157  @Test
158  public void testConcurrentAccess() throws InterruptedException {
159    setupTestMetrics();
160
161    int numThreads = 10;
162    int incrementsPerThread = 100;
163
164    ExecutorService executor = Executors.newFixedThreadPool(numThreads);
165    CountDownLatch startLatch = new CountDownLatch(1);
166    CountDownLatch doneLatch = new CountDownLatch(numThreads);
167    AtomicInteger exceptions = new AtomicInteger(0);
168
169    // Create multiple threads that increment the same counter concurrently
170    for (int i = 0; i < numThreads; i++) {
171      executor.submit(() -> {
172        try {
173          startLatch.await();
174          for (int j = 0; j < incrementsPerThread; j++) {
175            throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded,
176              "alice", "users");
177          }
178        } catch (Exception e) {
179          exceptions.incrementAndGet();
180        } finally {
181          doneLatch.countDown();
182        }
183      });
184    }
185
186    // Start all threads at once
187    startLatch.countDown();
188
189    // Wait for all threads to complete
190    boolean completed = doneLatch.await(30, TimeUnit.SECONDS);
191    assertTrue("All threads should complete within timeout", completed);
192    assertEquals("No exceptions should occur during concurrent access", 0, exceptions.get());
193
194    // Verify the final counter value
195    verifyCounter(testRegistry,
196      "RpcThrottlingException_Type_NumRequestsExceeded_User_alice_Table_users",
197      numThreads * incrementsPerThread);
198
199    executor.shutdown();
200  }
201
202  @Test
203  public void testCommonTableNamePatterns() {
204    setupTestMetrics();
205
206    // Test common HBase table name patterns that should be preserved
207    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded,
208      "service-user", "my-app-logs");
209    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.WriteSizeExceeded,
210      "batch.process", "namespace:table-name");
211    throttleMetrics.recordThrottleException(RpcThrottlingException.Type.ReadSizeExceeded,
212      "user_123", "test_table_v2");
213
214    // Verify common patterns are preserved correctly (note: colon gets replaced with underscore)
215    verifyCounter(testRegistry,
216      "RpcThrottlingException_Type_NumRequestsExceeded_User_service-user_Table_my-app-logs", 1);
217    verifyCounter(testRegistry,
218      "RpcThrottlingException_Type_WriteSizeExceeded_User_batch.process_Table_namespace_table-name",
219      1);
220    verifyCounter(testRegistry,
221      "RpcThrottlingException_Type_ReadSizeExceeded_User_user_123_Table_test_table_v2", 1);
222  }
223
224  @Test
225  public void testAllThrottleExceptionTypes() {
226    setupTestMetrics();
227
228    // Test all 13 throttle exception types from RpcThrottlingException.Type enum
229    RpcThrottlingException.Type[] throttleTypes = RpcThrottlingException.Type.values();
230
231    // Record one exception for each type
232    for (RpcThrottlingException.Type throttleType : throttleTypes) {
233      throttleMetrics.recordThrottleException(throttleType, "testuser", "testtable");
234    }
235
236    // Verify all counters were created with correct values
237    for (RpcThrottlingException.Type throttleType : throttleTypes) {
238      String expectedMetricName =
239        "RpcThrottlingException_Type_" + throttleType.name() + "_User_testuser_Table_testtable";
240      verifyCounter(testRegistry, expectedMetricName, 1);
241    }
242  }
243
244  @Test
245  public void testMultipleInstances() {
246    setupTestMetrics();
247
248    // Test that multiple instances of MetricsThrottleExceptions work with the same registry
249    MetricsThrottleExceptions metrics1 = new MetricsThrottleExceptions(testRegistry);
250    MetricsThrottleExceptions metrics2 = new MetricsThrottleExceptions(testRegistry);
251
252    // Record different exceptions on each instance
253    metrics1.recordThrottleException(RpcThrottlingException.Type.NumRequestsExceeded, "alice",
254      "table1");
255    metrics2.recordThrottleException(RpcThrottlingException.Type.WriteSizeExceeded, "bob",
256      "table2");
257
258    // Verify both counters exist in the shared registry
259    verifyCounter(testRegistry,
260      "RpcThrottlingException_Type_NumRequestsExceeded_User_alice_Table_table1", 1);
261    verifyCounter(testRegistry,
262      "RpcThrottlingException_Type_WriteSizeExceeded_User_bob_Table_table2", 1);
263  }
264
265  /**
266   * Helper method to set up test metrics registry and instance
267   */
268  private void setupTestMetrics() {
269    MetricRegistryInfo registryInfo = getRegistryInfo();
270    testRegistry = MetricRegistries.global().create(registryInfo);
271    throttleMetrics = new MetricsThrottleExceptions(testRegistry);
272  }
273
274  /**
275   * Helper method to verify a counter exists and has the expected value
276   */
277  private void verifyCounter(MetricRegistry registry, String metricName, long expectedCount) {
278    Optional<Metric> metric = registry.get(metricName);
279    assertTrue("Counter metric '" + metricName + "' should be present", metric.isPresent());
280    assertTrue("Metric should be a counter", metric.get() instanceof Counter);
281
282    Counter counter = (Counter) metric.get();
283    assertEquals("Counter '" + metricName + "' should have expected count", expectedCount,
284      counter.getCount());
285  }
286
287  /**
288   * Helper method to create the expected MetricRegistryInfo for ThrottleExceptions
289   */
290  private MetricRegistryInfo getRegistryInfo() {
291    return new MetricRegistryInfo("ThrottleExceptions", "Metrics about RPC throttling exceptions",
292      "RegionServer,sub=ThrottleExceptions", "regionserver", false);
293  }
294}