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}