This is an automated email from the ASF dual-hosted git repository.

ruanwenjun pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 386fc0ace2 [Fix-18330] Replace HashMap with ConcurrentHashMap in 
UserGroupInformationFactory to avoid ConcurrentModificationException (#18331)
386fc0ace2 is described below

commit 386fc0ace2955bfcf0151729a484496e1bdd93e6
Author: eye-gu <[email protected]>
AuthorDate: Wed Jun 10 20:28:15 2026 +0800

    [Fix-18330] Replace HashMap with ConcurrentHashMap in 
UserGroupInformationFactory to avoid ConcurrentModificationException (#18331)
---
 .../hive/security/UserGroupInformationFactory.java |   3 +-
 .../security/UserGroupInformationFactoryTest.java  | 126 +++++++++++++++++++++
 2 files changed, 128 insertions(+), 1 deletion(-)

diff --git 
a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/security/UserGroupInformationFactory.java
 
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/security/UserGroupInformationFactory.java
index 8e9469c169..50b74ccb24 100644
--- 
a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/security/UserGroupInformationFactory.java
+++ 
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/main/java/org/apache/dolphinscheduler/plugin/datasource/hive/security/UserGroupInformationFactory.java
@@ -29,6 +29,7 @@ import org.apache.hadoop.security.UserGroupInformation;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
@@ -39,7 +40,7 @@ public class UserGroupInformationFactory {
 
     private static final Map<String, Integer> currentLoginTimesMap = new 
HashMap<>();
 
-    private static final Map<String, UserGroupInformation> 
userGroupInformationMap = new HashMap<>();
+    private static final Map<String, UserGroupInformation> 
userGroupInformationMap = new ConcurrentHashMap<>();
 
     private static final ScheduledExecutorService kerberosRenewalService =
             
ThreadUtils.newSingleDaemonScheduledExecutorService("Hive-Kerberos-Renewal-Thread-");
diff --git 
a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/test/java/org/apache/dolphinscheduler/plugin/datasource/hive/security/UserGroupInformationFactoryTest.java
 
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/test/java/org/apache/dolphinscheduler/plugin/datasource/hive/security/UserGroupInformationFactoryTest.java
new file mode 100644
index 0000000000..1d2218da91
--- /dev/null
+++ 
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-hive/src/test/java/org/apache/dolphinscheduler/plugin/datasource/hive/security/UserGroupInformationFactoryTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dolphinscheduler.plugin.datasource.hive.security;
+
+import org.apache.dolphinscheduler.common.constants.Constants;
+import org.apache.dolphinscheduler.common.utils.PropertyUtils;
+
+import org.apache.hadoop.security.UserGroupInformation;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class UserGroupInformationFactoryTest {
+
+    private MockedStatic<PropertyUtils> mockedPropertyUtils;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        mockedPropertyUtils = Mockito.mockStatic(PropertyUtils.class);
+        mockedPropertyUtils
+                .when(() -> 
PropertyUtils.getUpperCaseString(Constants.RESOURCE_STORAGE_TYPE))
+                .thenReturn("LOCAL");
+        clearInternalMaps();
+    }
+
+    @AfterEach
+    void tearDown() {
+        mockedPropertyUtils.close();
+    }
+
+    @Test
+    void testForEachConcurrentWithModification() throws Exception {
+        for (int i = 0; i < 10; i++) {
+            UserGroupInformationFactory.login("user_" + i);
+        }
+
+        CountDownLatch startLatch = new CountDownLatch(1);
+        CountDownLatch doneLatch = new CountDownLatch(2);
+        AtomicInteger errorCount = new AtomicInteger(0);
+
+        Thread iteratorThread = new Thread(() -> {
+            try {
+                startLatch.await();
+                for (int round = 0; round < 10000; round++) {
+                    getUserGroupInformationMap().forEach((key, ugi) -> {
+                    });
+                }
+            } catch (Exception e) {
+                errorCount.incrementAndGet();
+            } finally {
+                doneLatch.countDown();
+            }
+        });
+        iteratorThread.start();
+
+        Thread modifierPool = new Thread(() -> {
+            try {
+                startLatch.await();
+                for (int i = 0; i < 500; i++) {
+                    for (int j = 0; j < 10; j++) {
+                        String user = "user_" + j;
+                        UserGroupInformationFactory.logout(user);
+                        UserGroupInformationFactory.login(user);
+                    }
+                }
+            } catch (Exception e) {
+                errorCount.incrementAndGet();
+            } finally {
+                doneLatch.countDown();
+            }
+        });
+        modifierPool.start();
+
+        startLatch.countDown();
+        boolean completed = doneLatch.await(30, TimeUnit.SECONDS);
+        Assertions.assertTrue(completed, "Test should complete within 
timeout");
+        Assertions.assertEquals(0, errorCount.get(),
+                "No ConcurrentModificationException should occur during 
concurrent forEach and modification");
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Map<String, UserGroupInformation> 
getUserGroupInformationMap() throws Exception {
+        Field field = 
UserGroupInformationFactory.class.getDeclaredField("userGroupInformationMap");
+        field.setAccessible(true);
+        return (Map<String, UserGroupInformation>) field.get(null);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void clearInternalMaps() throws Exception {
+        Field userGroupInfoMapField = 
UserGroupInformationFactory.class.getDeclaredField("userGroupInformationMap");
+        userGroupInfoMapField.setAccessible(true);
+        ((Map<String, ?>) userGroupInfoMapField.get(null)).clear();
+
+        Field currentLoginTimesMapField = 
UserGroupInformationFactory.class.getDeclaredField("currentLoginTimesMap");
+        currentLoginTimesMapField.setAccessible(true);
+        ((Map<String, ?>) currentLoginTimesMapField.get(null)).clear();
+    }
+}

Reply via email to