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