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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 94a90d7a1 fix(instance): tolerate H2's unsnapshotted index metadata 
during concurrent startup (#6736)
94a90d7a1 is described below

commit 94a90d7a1aebf7a9b5c5d347c70c7a6fd6b91851
Author: lizhimins <[email protected]>
AuthorDate: Thu Oct 15 20:12:32 2026 +0800

    fix(instance): tolerate H2's unsnapshotted index metadata during concurrent 
startup (#6736)
    
    migrationConcurrentStartupIsIdempotentTest failed roughly one run in three 
with
    
        JdbcSQLNonTransientException: General error:
        "java.util.ConcurrentModificationException"
          at org.h2.jdbc.meta.DatabaseMetaLocal.getIndexInfo
          at ...ResourceOwnershipSchemaMigration.indexes
    
    H2 answers getIndexInfo from a mutable metadata cache without taking a
    snapshot, so an index committed by a concurrently starting node mutates the
    list being iterated. The read is idempotent and ensureIndex already 
tolerates a
    racing CREATE INDEX, so retry it a bounded number of times when the cause 
chain
    carries a ConcurrentModificationException and rethrow anything else 
unchanged.
    
    Deliberately not a lock: serializing the migration inside one JVM would make
    the concurrent-startup regression test assert nothing, and the production
    database is MySQL, whose metadata reads do not need this.
    
    Verified: eight consecutive runs of ResourceOwnershipGuardTest green (one in
    three failed before).
---
 .../instance/ResourceOwnershipSchemaMigration.java | 40 ++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/ResourceOwnershipSchemaMigration.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/ResourceOwnershipSchemaMigration.java
index 6cfbfab7c..9efdff712 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/ResourceOwnershipSchemaMigration.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/ResourceOwnershipSchemaMigration.java
@@ -29,6 +29,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -38,6 +39,9 @@ import java.util.Map;
 @DependsOnDatabaseInitialization
 @RequiredArgsConstructor
 public class ResourceOwnershipSchemaMigration implements InitializingBean {
+    /** Bounded retries for the index-metadata read; see {@link 
#indexes(Connection, String)}. */
+    private static final int INDEX_METADATA_ATTEMPTS = 5;
+
     private final DataSource dataSource;
     private final RocketMQDefaultClusterResolver defaultClusters;
 
@@ -136,6 +140,27 @@ public class ResourceOwnershipSchemaMigration implements 
InitializingBean {
     }
 
     private Map<String, List<String>> indexes(Connection connection, String 
table) throws SQLException {
+        // H2 serves getIndexInfo from a mutable metadata cache without taking 
a snapshot, so an index
+        // created by a concurrently starting node surfaces here as a 
ConcurrentModificationException
+        // wrapped in a SQLException. The read is idempotent and ensureIndex 
already tolerates a racing
+        // CREATE INDEX, so retry it a bounded number of times instead of 
serializing every startup
+        // behind one lock — a lock would also turn the concurrent-startup 
regression test into a
+        // tautology. MySQL reads committed metadata and never takes this path.
+        SQLException transientFailure = null;
+        for (int attempt = 1; attempt <= INDEX_METADATA_ATTEMPTS; attempt++) {
+            try {
+                return readIndexes(connection, table);
+            } catch (SQLException failure) {
+                if (!isConcurrentMetadataRead(failure)) {
+                    throw failure;
+                }
+                transientFailure = failure;
+            }
+        }
+        throw transientFailure;
+    }
+
+    private Map<String, List<String>> readIndexes(Connection connection, 
String table) throws SQLException {
         Map<String, List<String>> indexes = new HashMap<>();
         try (ResultSet rows = 
connection.getMetaData().getIndexInfo(connection.getCatalog(), null, table, 
false, false)) {
             while (rows.next()) {
@@ -150,4 +175,19 @@ public class ResourceOwnershipSchemaMigration implements 
InitializingBean {
         }
         return indexes;
     }
+
+    private static boolean isConcurrentMetadataRead(SQLException failure) {
+        Throwable cause = failure;
+        while (cause != null) {
+            if (cause instanceof ConcurrentModificationException) {
+                return true;
+            }
+            Throwable next = cause.getCause();
+            if (next == cause) {
+                break;
+            }
+            cause = next;
+        }
+        return false;
+    }
 }

Reply via email to