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}