This is an automated email from the ASF dual-hosted git repository.
dockerzhang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git
The following commit(s) were added to refs/heads/master by this push:
new f89c9a175d [INLONG-12137][Audit] Resolve thread-safety issues in the
Audit SDK under high concurrency (#12138)
f89c9a175d is described below
commit f89c9a175dddf82ce2c5fdf1864cc377bdc25547
Author: doleyzi <[email protected]>
AuthorDate: Wed Jun 3 17:02:58 2026 +0800
[INLONG-12137][Audit] Resolve thread-safety issues in the Audit SDK under
high concurrency (#12138)
Co-authored-by: doleyzi <[email protected]>
---
.../org/apache/inlong/audit/AuditReporterImpl.java | 17 +-
.../apache/inlong/audit/AuditReporterImplTest.java | 223 +++++++++++++++++++++
2 files changed, 232 insertions(+), 8 deletions(-)
diff --git
a/inlong-audit/audit-sdk/src/main/java/org/apache/inlong/audit/AuditReporterImpl.java
b/inlong-audit/audit-sdk/src/main/java/org/apache/inlong/audit/AuditReporterImpl.java
index 2efa69d6fc..f787cd78d6 100644
---
a/inlong-audit/audit-sdk/src/main/java/org/apache/inlong/audit/AuditReporterImpl.java
+++
b/inlong-audit/audit-sdk/src/main/java/org/apache/inlong/audit/AuditReporterImpl.java
@@ -298,16 +298,17 @@ public class AuditReporterImpl implements Serializable {
/**
* Add audit info by key.
+ *
+ * Use {@link ConcurrentHashMap#computeIfAbsent} to atomically
get-or-create
+ * the inner map and the {@link StatInfo} entry, avoiding the race
condition
+ * where a concurrent flush/clearExpiredKey removes the entry between
+ * {@code putIfAbsent} and the subsequent {@code get}, which could
otherwise
+ * cause a NullPointerException or silent data loss.
*/
private void addByKey(long isolateKey, String statKey, long count, long
size, long delayTime) {
- if (null == this.preStatMap.get(isolateKey)) {
- this.preStatMap.putIfAbsent(isolateKey, new ConcurrentHashMap<>());
- }
- ConcurrentHashMap<String, StatInfo> statMap =
this.preStatMap.get(isolateKey);
- if (null == statMap.get(statKey)) {
- statMap.putIfAbsent(statKey, new StatInfo(0L, 0L, 0L));
- }
- StatInfo stat = statMap.get(statKey);
+ ConcurrentHashMap<String, StatInfo> statMap =
+ this.preStatMap.computeIfAbsent(isolateKey, k -> new
ConcurrentHashMap<>());
+ StatInfo stat = statMap.computeIfAbsent(statKey, k -> new StatInfo(0L,
0L, 0L));
stat.count.addAndGet(count);
stat.size.addAndGet(size);
stat.delay.addAndGet(delayTime);
diff --git
a/inlong-audit/audit-sdk/src/test/java/org/apache/inlong/audit/AuditReporterImplTest.java
b/inlong-audit/audit-sdk/src/test/java/org/apache/inlong/audit/AuditReporterImplTest.java
index e579466dc6..0c2fe7e5b3 100644
---
a/inlong-audit/audit-sdk/src/test/java/org/apache/inlong/audit/AuditReporterImplTest.java
+++
b/inlong-audit/audit-sdk/src/test/java/org/apache/inlong/audit/AuditReporterImplTest.java
@@ -17,11 +17,25 @@
package org.apache.inlong.audit;
+import org.apache.inlong.audit.util.StatInfo;
+
import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
import static org.apache.inlong.audit.AuditIdEnum.AGENT_INPUT;
import static org.apache.inlong.audit.AuditIdEnum.SORT_HIVE_INPUT;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
public class AuditReporterImplTest {
@@ -45,4 +59,213 @@ public class AuditReporterImplTest {
auditId =
AuditOperator.getInstance().buildRetryAuditId(SORT_HIVE_INPUT, false);
assertEquals(327687, auditId);
}
+
+ /**
+ * Invoke the private addByKey method via reflection so that its behavior
can be tested directly.
+ */
+ private static void invokeAddByKey(AuditReporterImpl reporter, long
isolateKey,
+ String statKey, long count, long size, long delayTime) throws
Exception {
+ Method method = AuditReporterImpl.class.getDeclaredMethod(
+ "addByKey", long.class, String.class, long.class, long.class,
long.class);
+ method.setAccessible(true);
+ method.invoke(reporter, isolateKey, statKey, count, size, delayTime);
+ }
+
+ /**
+ * Get the preStatMap via reflection so that the internal state can be
verified in tests.
+ */
+ @SuppressWarnings("unchecked")
+ private static ConcurrentHashMap<Long, ConcurrentHashMap<String,
StatInfo>> getPreStatMap(
+ AuditReporterImpl reporter) throws Exception {
+ Field field = AuditReporterImpl.class.getDeclaredField("preStatMap");
+ field.setAccessible(true);
+ return (ConcurrentHashMap<Long, ConcurrentHashMap<String, StatInfo>>)
field.get(reporter);
+ }
+
+ /**
+ * Verify that on the first invocation the inner map and StatInfo are
created correctly,
+ * and count/size/delay are accumulated on top of 0.
+ */
+ @Test
+ public void testAddByKey_firstInsert() throws Exception {
+ AuditReporterImpl reporter = new AuditReporterImpl();
+ long isolateKey = 1L;
+ String statKey = "1700000000:groupA:streamA:1:tagA:-1";
+
+ invokeAddByKey(reporter, isolateKey, statKey, 10L, 100L, 5L);
+
+ ConcurrentHashMap<Long, ConcurrentHashMap<String, StatInfo>>
preStatMap = getPreStatMap(reporter);
+ assertNotNull("preStatMap should not be null", preStatMap);
+ assertNotNull("inner map should be created",
preStatMap.get(isolateKey));
+ StatInfo stat = preStatMap.get(isolateKey).get(statKey);
+ assertNotNull("StatInfo should be created", stat);
+ assertEquals(10L, stat.count.get());
+ assertEquals(100L, stat.size.get());
+ assertEquals(5L, stat.delay.get());
+ }
+
+ /**
+ * Verify that count/size/delay are accumulated strictly when addByKey is
called multiple
+ * times on the same (isolateKey, statKey).
+ */
+ @Test
+ public void testAddByKey_accumulateOnSameKey() throws Exception {
+ AuditReporterImpl reporter = new AuditReporterImpl();
+ long isolateKey = 1L;
+ String statKey = "logtime:group:stream:auditId:tag:0";
+
+ invokeAddByKey(reporter, isolateKey, statKey, 1L, 10L, 100L);
+ invokeAddByKey(reporter, isolateKey, statKey, 2L, 20L, 200L);
+ invokeAddByKey(reporter, isolateKey, statKey, 3L, 30L, 300L);
+
+ StatInfo stat = getPreStatMap(reporter).get(isolateKey).get(statKey);
+ assertEquals(6L, stat.count.get());
+ assertEquals(60L, stat.size.get());
+ assertEquals(600L, stat.delay.get());
+ }
+
+ /**
+ * Verify that computeIfAbsent does not overwrite the existing StatInfo
instance when the
+ * key is already present.
+ */
+ @Test
+ public void testAddByKey_reuseExistingStatInfo() throws Exception {
+ AuditReporterImpl reporter = new AuditReporterImpl();
+ long isolateKey = 2L;
+ String statKey = "key-reuse";
+
+ invokeAddByKey(reporter, isolateKey, statKey, 1L, 1L, 1L);
+ StatInfo first = getPreStatMap(reporter).get(isolateKey).get(statKey);
+
+ invokeAddByKey(reporter, isolateKey, statKey, 5L, 50L, 500L);
+ StatInfo second = getPreStatMap(reporter).get(isolateKey).get(statKey);
+
+ assertSame("StatInfo instance must be reused, not replaced", first,
second);
+ assertEquals(6L, second.count.get());
+ assertEquals(51L, second.size.get());
+ assertEquals(501L, second.delay.get());
+ }
+
+ /**
+ * Verify that different statKeys under the same isolateKey are isolated
from each other.
+ */
+ @Test
+ public void testAddByKey_isolationBetweenStatKeys() throws Exception {
+ AuditReporterImpl reporter = new AuditReporterImpl();
+ long isolateKey = 1L;
+ String keyA = "statKey-A";
+ String keyB = "statKey-B";
+
+ invokeAddByKey(reporter, isolateKey, keyA, 1L, 10L, 100L);
+ invokeAddByKey(reporter, isolateKey, keyB, 2L, 20L, 200L);
+
+ ConcurrentHashMap<String, StatInfo> innerMap =
getPreStatMap(reporter).get(isolateKey);
+ assertEquals(2, innerMap.size());
+ assertEquals(1L, innerMap.get(keyA).count.get());
+ assertEquals(10L, innerMap.get(keyA).size.get());
+ assertEquals(100L, innerMap.get(keyA).delay.get());
+ assertEquals(2L, innerMap.get(keyB).count.get());
+ assertEquals(20L, innerMap.get(keyB).size.get());
+ assertEquals(200L, innerMap.get(keyB).delay.get());
+ }
+
+ /**
+ * Verify that different isolateKeys are isolated from each other and use
independent
+ * inner maps.
+ */
+ @Test
+ public void testAddByKey_isolationBetweenIsolateKeys() throws Exception {
+ AuditReporterImpl reporter = new AuditReporterImpl();
+ String statKey = "shared-stat-key";
+
+ invokeAddByKey(reporter, 1L, statKey, 1L, 10L, 100L);
+ invokeAddByKey(reporter, 2L, statKey, 7L, 70L, 700L);
+
+ ConcurrentHashMap<Long, ConcurrentHashMap<String, StatInfo>>
preStatMap = getPreStatMap(reporter);
+ assertEquals(2, preStatMap.size());
+
+ StatInfo stat1 = preStatMap.get(1L).get(statKey);
+ StatInfo stat2 = preStatMap.get(2L).get(statKey);
+ assertEquals(1L, stat1.count.get());
+ assertEquals(10L, stat1.size.get());
+ assertEquals(100L, stat1.delay.get());
+ assertEquals(7L, stat2.count.get());
+ assertEquals(70L, stat2.size.get());
+ assertEquals(700L, stat2.delay.get());
+ }
+
+ /**
+ * Verify thread safety of concurrent accumulation on the same
(isolateKey, statKey),
+ * i.e. after the fix there should be no NPE and no lost updates.
+ */
+ @Test
+ public void testAddByKey_concurrentSafety() throws Exception {
+ final AuditReporterImpl reporter = new AuditReporterImpl();
+ final long isolateKey = 1L;
+ final String statKey = "concurrent-key";
+
+ final int threadNum = 16;
+ final int loopPerThread = 5000;
+ final long countPerOp = 1L;
+ final long sizePerOp = 2L;
+ final long delayPerOp = 3L;
+
+ ExecutorService pool = Executors.newFixedThreadPool(threadNum);
+ final CountDownLatch start = new CountDownLatch(1);
+ final CountDownLatch done = new CountDownLatch(threadNum);
+ final AtomicInteger errors = new AtomicInteger(0);
+
+ for (int i = 0; i < threadNum; i++) {
+ pool.submit(() -> {
+ try {
+ start.await();
+ for (int j = 0; j < loopPerThread; j++) {
+ invokeAddByKey(reporter, isolateKey, statKey,
countPerOp, sizePerOp, delayPerOp);
+ }
+ } catch (Exception e) {
+ errors.incrementAndGet();
+ } finally {
+ done.countDown();
+ }
+ });
+ }
+
+ start.countDown();
+ assertTrue("threads should finish in time", done.await(30,
TimeUnit.SECONDS));
+ pool.shutdownNow();
+
+ assertEquals("no exception expected during concurrent addByKey", 0,
errors.get());
+
+ StatInfo stat = getPreStatMap(reporter).get(isolateKey).get(statKey);
+ assertNotNull(stat);
+ long expectedOps = (long) threadNum * loopPerThread;
+ assertEquals(expectedOps * countPerOp, stat.count.get());
+ assertEquals(expectedOps * sizePerOp, stat.size.get());
+ assertEquals(expectedOps * delayPerOp, stat.delay.get());
+ }
+
+ /**
+ * Verify that calls through the public add(...) entry point ultimately
land in preStatMap.
+ * This is an end-to-end integration check for addByKey.
+ */
+ @Test
+ public void testAddByKey_viaPublicAddApi() throws Exception {
+ AuditReporterImpl reporter = new AuditReporterImpl();
+ long logTime = 1700000000000L;
+ int auditId = 1;
+ String groupId = "group1";
+ String streamId = "stream1";
+
+ reporter.add(auditId, groupId, streamId, logTime, 8L, 80L);
+
+ ConcurrentHashMap<Long, ConcurrentHashMap<String, StatInfo>>
preStatMap = getPreStatMap(reporter);
+ ConcurrentHashMap<String, StatInfo> innerMap =
+ preStatMap.get(AuditReporterImpl.DEFAULT_ISOLATE_KEY);
+ assertNotNull("inner map under DEFAULT_ISOLATE_KEY should exist",
innerMap);
+ assertEquals(1, innerMap.size());
+
+ StatInfo stat = innerMap.values().iterator().next();
+ assertEquals(8L, stat.count.get());
+ assertEquals(80L, stat.size.get());
+ }
}