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

jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new 4a66d8cce0 [Cherry-pick to branch-1.3] [#13206] improvement(core): 
lock the entity node instead of the schema when creating tables, topics, views 
and models (#13207) (#13210)
4a66d8cce0 is described below

commit 4a66d8cce0c6a8756d5c96425828b1b1221e763b
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Sep 16 20:38:33 2026 +0800

    [Cherry-pick to branch-1.3] [#13206] improvement(core): lock the entity 
node instead of the schema when creating tables, topics, views and models 
(#13207) (#13210)
    
    **Cherry-pick Information:**
    - Original commit: 2928e0f0f31d765360298f379f0439c182c5f02f
    - Target branch: `branch-1.3`
    - Status: Conflicts resolved
    
    Resolved the model dispatcher test conflict by retaining the new
    tree-lock tests and excluding an unrelated main-branch property
    assertion helper.
    
    Validation: `spotlessApply` and all 88 tests across the table, topic,
    view, and model operation dispatcher test suites passed.
    
    ---------
    
    Co-authored-by: Qi Yu <[email protected]>
---
 .../catalog/ModelOperationDispatcher.java          |   4 +-
 .../catalog/TableOperationDispatcher.java          |   8 +-
 .../catalog/TopicOperationDispatcher.java          |   6 +-
 .../gravitino/catalog/ViewOperationDispatcher.java |   4 +-
 .../catalog/TestModelOperationDispatcher.java      |  67 +++++++
 .../catalog/TestTableOperationDispatcher.java      |  68 +++++++
 .../catalog/TestTopicOperationDispatcher.java      |  68 +++++++
 .../catalog/TestViewOperationDispatcher.java       |  77 ++++++++
 .../gravitino/catalog/TreeLockTestSupport.java     | 214 +++++++++++++++++++++
 9 files changed, 510 insertions(+), 6 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ModelOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ModelOperationDispatcher.java
index f3d3389098..55a90f6351 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/ModelOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ModelOperationDispatcher.java
@@ -93,9 +93,11 @@ public class ModelOperationDispatcher extends 
OperationDispatcher implements Mod
         checkAndUpdateProperties(
             catalogIdent, properties, 
HasPropertyMetadata::modelPropertiesMetadata);
 
+    // Lock the model node, not the schema, so models in the same schema can 
be registered
+    // concurrently. See TableOperationDispatcher#createTable for the 
reasoning and trade-off.
     Model registeredModel =
         TreeLockUtils.doWithTreeLock(
-            NameIdentifier.of(ident.namespace().levels()),
+            ident,
             LockType.WRITE,
             () ->
                 doWithCatalog(
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
index 147c2da544..3cdcd77b43 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
@@ -208,8 +208,14 @@ public class TableOperationDispatcher extends 
OperationDispatcher implements Tab
     NameIdentifier schemaIdent = NameIdentifier.of(ident.namespace().levels());
     schemaDispatcher.loadSchema(schemaIdent);
 
+    // Lock the table node, not the schema, so tables in the same schema can 
be created
+    // concurrently. The connector call and the entity-store write otherwise 
run under one
+    // schema-wide lock, which serialized all creates in a schema. Ancestors 
are still read-locked,
+    // so create keeps excluding dropSchema/createSchema (catalog WRITE) and 
rename/drop/import
+    // (schema WRITE); a same-name create, load or alter contends on the table 
node.
+    // Trade-off: listTables may briefly observe a table whose creation has 
not committed yet.
     return TreeLockUtils.doWithTreeLock(
-        NameIdentifier.of(ident.namespace().levels()),
+        ident,
         LockType.WRITE,
         () ->
             internalCreateTable(
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/TopicOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/TopicOperationDispatcher.java
index 3c43860701..77e136969d 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/TopicOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/TopicOperationDispatcher.java
@@ -138,10 +138,10 @@ public class TopicOperationDispatcher extends 
OperationDispatcher implements Top
     NameIdentifier schemaIdent = NameIdentifier.of(ident.namespace().levels());
     schemaDispatcher.loadSchema(schemaIdent);
 
+    // Lock the topic node, not the schema, so topics in the same schema can 
be created
+    // concurrently. See TableOperationDispatcher#createTable for the 
reasoning and trade-off.
     return TreeLockUtils.doWithTreeLock(
-        NameIdentifier.of(ident.namespace().levels()),
-        LockType.WRITE,
-        () -> internalCreateTopic(ident, comment, dataLayout, properties));
+        ident, LockType.WRITE, () -> internalCreateTopic(ident, comment, 
dataLayout, properties));
   }
 
   /**
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
index 844a5e5f34..557819b4dc 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
@@ -178,8 +178,10 @@ public class ViewOperationDispatcher extends 
OperationDispatcher implements View
     NameIdentifier schemaIdent = NameIdentifier.of(ident.namespace().levels());
     schemaDispatcher.loadSchema(schemaIdent);
 
+    // Lock the view node, not the schema, so views in the same schema can be 
created
+    // concurrently. See TableOperationDispatcher#createTable for the 
reasoning and trade-off.
     return TreeLockUtils.doWithTreeLock(
-        schemaIdent,
+        ident,
         LockType.WRITE,
         () ->
             internalCreateView(
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
index c9f2cb069a..27a4639360 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
@@ -40,6 +40,7 @@ import org.apache.gravitino.exceptions.NoSuchModelException;
 import org.apache.gravitino.exceptions.NoSuchModelVersionException;
 import org.apache.gravitino.exceptions.NoSuchModelVersionURINameException;
 import org.apache.gravitino.lock.LockManager;
+import org.apache.gravitino.lock.LockType;
 import org.apache.gravitino.model.Model;
 import org.apache.gravitino.model.ModelChange;
 import org.apache.gravitino.model.ModelVersion;
@@ -1287,4 +1288,70 @@ public class TestModelOperationDispatcher extends 
TestOperationDispatcher {
   private String randomModelName() {
     return "model_" + UUID.randomUUID().toString().replace("-", "");
   }
+
+  @Test
+  public void 
testRegisterModelRunsConcurrentlyWithRegisterModelOfAnotherModel() throws 
Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_model_lock_1");
+    createSchemaForLockTest(schemaIdent);
+
+    // Another in-flight create holds the WRITE lock on its own model node.
+    try (TreeLockTestSupport.HeldLock inFlightCreate =
+        TreeLockTestSupport.HeldLock.acquire(
+            NameIdentifier.of(metalake, catalog, "schema_model_lock_1", 
"other_model"),
+            LockType.WRITE)) {
+      TreeLockTestSupport.assertRunsConcurrentlyWith(
+          inFlightCreate,
+          () ->
+              registerModelForLockTest(
+                  NameIdentifier.of(metalake, catalog, "schema_model_lock_1", 
"model1")));
+    }
+  }
+
+  @Test
+  public void testRegisterModelWaitsForRegisterModelOfSameName() throws 
Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_model_lock_2");
+    createSchemaForLockTest(schemaIdent);
+    NameIdentifier ident = NameIdentifier.of(metalake, catalog, 
"schema_model_lock_2", "model1");
+
+    TreeLockTestSupport.HeldLock sameNameCreate =
+        TreeLockTestSupport.HeldLock.acquire(ident, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(sameNameCreate, () -> 
registerModelForLockTest(ident));
+  }
+
+  @Test
+  public void testRegisterModelWaitsForSchemaWriteLock() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_model_lock_3");
+    createSchemaForLockTest(schemaIdent);
+
+    TreeLockTestSupport.HeldLock schemaWriter =
+        TreeLockTestSupport.HeldLock.acquire(schemaIdent, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        schemaWriter,
+        () ->
+            registerModelForLockTest(
+                NameIdentifier.of(metalake, catalog, "schema_model_lock_3", 
"model1")));
+  }
+
+  @Test
+  public void testRegisterModelWaitsForCatalogWriteLock() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_model_lock_4");
+    createSchemaForLockTest(schemaIdent);
+
+    TreeLockTestSupport.HeldLock catalogWriter =
+        TreeLockTestSupport.HeldLock.acquire(NameIdentifier.of(metalake, 
catalog), LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        catalogWriter,
+        () ->
+            registerModelForLockTest(
+                NameIdentifier.of(metalake, catalog, "schema_model_lock_4", 
"model1")));
+  }
+
+  private static void createSchemaForLockTest(NameIdentifier schemaIdent) {
+    schemaOperationDispatcher.createSchema(schemaIdent, "comment", null);
+  }
+
+  private static Model registerModelForLockTest(NameIdentifier ident) {
+    return modelOperationDispatcher.registerModel(
+        ident, "comment", ImmutableMap.of("k1", "v1", "k2", "v2"));
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
index 94b490f0a7..9977b132a1 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
@@ -60,6 +60,7 @@ import org.apache.gravitino.connector.TestCatalogOperations;
 import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.lock.LockManager;
+import org.apache.gravitino.lock.LockType;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.ColumnEntity;
 import org.apache.gravitino.meta.SchemaEntity;
@@ -1175,6 +1176,73 @@ public class TestTableOperationDispatcher extends 
TestOperationDispatcher {
     return tableOperationDispatcher;
   }
 
+  @Test
+  public void testCreateTableRunsConcurrentlyWithCreateOfAnotherTable() throws 
Exception {
+    Namespace tableNs = Namespace.of(metalake, catalog, 
"schema_create_lock_1");
+    schemaOperationDispatcher.createSchema(
+        NameIdentifier.of(tableNs.levels()), "comment", ImmutableMap.of("k1", 
"v1", "k2", "v2"));
+
+    // Another in-flight create holds the WRITE lock on its own table node.
+    try (TreeLockTestSupport.HeldLock inFlightCreate =
+        TreeLockTestSupport.HeldLock.acquire(
+            NameIdentifier.of(tableNs, "other_table"), LockType.WRITE)) {
+      TreeLockTestSupport.assertRunsConcurrentlyWith(
+          inFlightCreate, () -> createTable(NameIdentifier.of(tableNs, 
"table1")));
+    }
+  }
+
+  @Test
+  public void testCreateTableWaitsForCreateOfSameName() throws Exception {
+    Namespace tableNs = Namespace.of(metalake, catalog, 
"schema_create_lock_2");
+    schemaOperationDispatcher.createSchema(
+        NameIdentifier.of(tableNs.levels()), "comment", ImmutableMap.of("k1", 
"v1", "k2", "v2"));
+    NameIdentifier tableIdent = NameIdentifier.of(tableNs, "table1");
+
+    TreeLockTestSupport.HeldLock sameNameCreate =
+        TreeLockTestSupport.HeldLock.acquire(tableIdent, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(sameNameCreate, () -> 
createTable(tableIdent));
+  }
+
+  @Test
+  public void testCreateTableWaitsForSchemaWriteLock() throws Exception {
+    // Rename, drop and import of tables in the schema take the schema WRITE 
lock.
+    Namespace tableNs = Namespace.of(metalake, catalog, 
"schema_create_lock_3");
+    NameIdentifier schemaIdent = NameIdentifier.of(tableNs.levels());
+    schemaOperationDispatcher.createSchema(
+        schemaIdent, "comment", ImmutableMap.of("k1", "v1", "k2", "v2"));
+
+    TreeLockTestSupport.HeldLock schemaWriter =
+        TreeLockTestSupport.HeldLock.acquire(schemaIdent, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        schemaWriter, () -> createTable(NameIdentifier.of(tableNs, "table1")));
+  }
+
+  @Test
+  public void testCreateTableWaitsForCatalogWriteLock() throws Exception {
+    // dropSchema and createSchema take the catalog WRITE lock.
+    Namespace tableNs = Namespace.of(metalake, catalog, 
"schema_create_lock_4");
+    schemaOperationDispatcher.createSchema(
+        NameIdentifier.of(tableNs.levels()), "comment", ImmutableMap.of("k1", 
"v1", "k2", "v2"));
+
+    TreeLockTestSupport.HeldLock catalogWriter =
+        TreeLockTestSupport.HeldLock.acquire(NameIdentifier.of(metalake, 
catalog), LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        catalogWriter, () -> createTable(NameIdentifier.of(tableNs, 
"table1")));
+  }
+
+  private static Table createTable(NameIdentifier ident) {
+    Column[] columns =
+        new Column[] {
+          TestColumn.builder()
+              .withName("col1")
+              .withPosition(0)
+              .withType(Types.StringType.get())
+              .build()
+        };
+    return tableOperationDispatcher.createTable(
+        ident, columns, "comment", ImmutableMap.of("k1", "v1", "k2", "v2"), 
new Transform[0]);
+  }
+
   public static SchemaOperationDispatcher getSchemaOperationDispatcher() {
     return schemaOperationDispatcher;
   }
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
index b8b979e7e5..c043e8f0b0 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
@@ -50,6 +50,7 @@ import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.connector.TestCatalogOperations;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.lock.LockManager;
+import org.apache.gravitino.lock.LockType;
 import org.apache.gravitino.messaging.Topic;
 import org.apache.gravitino.messaging.TopicChange;
 import org.apache.gravitino.meta.AuditInfo;
@@ -282,4 +283,71 @@ public class TestTopicOperationDispatcher extends 
TestOperationDispatcher {
   public static TopicOperationDispatcher getTopicOperationDispatcher() {
     return topicOperationDispatcher;
   }
+
+  @Test
+  public void testCreateTopicRunsConcurrentlyWithCreateTopicOfAnotherTopic() 
throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_topic_lock_1");
+    createSchemaForLockTest(schemaIdent);
+
+    // Another in-flight create holds the WRITE lock on its own topic node.
+    try (TreeLockTestSupport.HeldLock inFlightCreate =
+        TreeLockTestSupport.HeldLock.acquire(
+            NameIdentifier.of(metalake, catalog, "schema_topic_lock_1", 
"other_topic"),
+            LockType.WRITE)) {
+      TreeLockTestSupport.assertRunsConcurrentlyWith(
+          inFlightCreate,
+          () ->
+              createTopicForLockTest(
+                  NameIdentifier.of(metalake, catalog, "schema_topic_lock_1", 
"topic1")));
+    }
+  }
+
+  @Test
+  public void testCreateTopicWaitsForCreateTopicOfSameName() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_topic_lock_2");
+    createSchemaForLockTest(schemaIdent);
+    NameIdentifier ident = NameIdentifier.of(metalake, catalog, 
"schema_topic_lock_2", "topic1");
+
+    TreeLockTestSupport.HeldLock sameNameCreate =
+        TreeLockTestSupport.HeldLock.acquire(ident, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(sameNameCreate, () -> 
createTopicForLockTest(ident));
+  }
+
+  @Test
+  public void testCreateTopicWaitsForSchemaWriteLock() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_topic_lock_3");
+    createSchemaForLockTest(schemaIdent);
+
+    TreeLockTestSupport.HeldLock schemaWriter =
+        TreeLockTestSupport.HeldLock.acquire(schemaIdent, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        schemaWriter,
+        () ->
+            createTopicForLockTest(
+                NameIdentifier.of(metalake, catalog, "schema_topic_lock_3", 
"topic1")));
+  }
+
+  @Test
+  public void testCreateTopicWaitsForCatalogWriteLock() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_topic_lock_4");
+    createSchemaForLockTest(schemaIdent);
+
+    TreeLockTestSupport.HeldLock catalogWriter =
+        TreeLockTestSupport.HeldLock.acquire(NameIdentifier.of(metalake, 
catalog), LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        catalogWriter,
+        () ->
+            createTopicForLockTest(
+                NameIdentifier.of(metalake, catalog, "schema_topic_lock_4", 
"topic1")));
+  }
+
+  private static void createSchemaForLockTest(NameIdentifier schemaIdent) {
+    schemaOperationDispatcher.createSchema(
+        schemaIdent, "comment", ImmutableMap.of("k1", "v1", "k2", "v2"));
+  }
+
+  private static Topic createTopicForLockTest(NameIdentifier ident) {
+    return topicOperationDispatcher.createTopic(
+        ident, "comment", null, ImmutableMap.of("k1", "v1", "k2", "v2"));
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
index e98e0a2804..4505de380f 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
@@ -53,6 +53,7 @@ import org.apache.gravitino.connector.TestCatalogOperations;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.exceptions.NoSuchViewException;
 import org.apache.gravitino.lock.LockManager;
+import org.apache.gravitino.lock.LockType;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.SchemaEntity;
 import org.apache.gravitino.meta.ViewEntity;
@@ -699,4 +700,80 @@ public class TestViewOperationDispatcher extends 
TestOperationDispatcher {
             .build();
     entityStore.put(entity, true);
   }
+
+  @Test
+  public void testCreateViewRunsConcurrentlyWithCreateViewOfAnotherView() 
throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_view_lock_1");
+    createSchemaForLockTest(schemaIdent);
+
+    // Another in-flight create holds the WRITE lock on its own view node.
+    try (TreeLockTestSupport.HeldLock inFlightCreate =
+        TreeLockTestSupport.HeldLock.acquire(
+            NameIdentifier.of(metalake, catalog, "schema_view_lock_1", 
"other_view"),
+            LockType.WRITE)) {
+      TreeLockTestSupport.assertRunsConcurrentlyWith(
+          inFlightCreate,
+          () ->
+              createViewForLockTest(
+                  NameIdentifier.of(metalake, catalog, "schema_view_lock_1", 
"view1")));
+    }
+  }
+
+  @Test
+  public void testCreateViewWaitsForCreateViewOfSameName() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_view_lock_2");
+    createSchemaForLockTest(schemaIdent);
+    NameIdentifier ident = NameIdentifier.of(metalake, catalog, 
"schema_view_lock_2", "view1");
+
+    TreeLockTestSupport.HeldLock sameNameCreate =
+        TreeLockTestSupport.HeldLock.acquire(ident, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(sameNameCreate, () -> 
createViewForLockTest(ident));
+  }
+
+  @Test
+  public void testCreateViewWaitsForSchemaWriteLock() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_view_lock_3");
+    createSchemaForLockTest(schemaIdent);
+
+    TreeLockTestSupport.HeldLock schemaWriter =
+        TreeLockTestSupport.HeldLock.acquire(schemaIdent, LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        schemaWriter,
+        () ->
+            createViewForLockTest(
+                NameIdentifier.of(metalake, catalog, "schema_view_lock_3", 
"view1")));
+  }
+
+  @Test
+  public void testCreateViewWaitsForCatalogWriteLock() throws Exception {
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
"schema_view_lock_4");
+    createSchemaForLockTest(schemaIdent);
+
+    TreeLockTestSupport.HeldLock catalogWriter =
+        TreeLockTestSupport.HeldLock.acquire(NameIdentifier.of(metalake, 
catalog), LockType.WRITE);
+    TreeLockTestSupport.assertWaitsFor(
+        catalogWriter,
+        () ->
+            createViewForLockTest(
+                NameIdentifier.of(metalake, catalog, "schema_view_lock_4", 
"view1")));
+  }
+
+  private static void createSchemaForLockTest(NameIdentifier schemaIdent) {
+    schemaOperationDispatcher.createSchema(
+        schemaIdent, "comment", ImmutableMap.of("k1", "v1", "k2", "v2"));
+  }
+
+  private static View createViewForLockTest(NameIdentifier ident) {
+    Representation[] representations = {
+      SQLRepresentation.builder().withDialect("spark").withSql("SELECT 
1").build()
+    };
+    return viewOperationDispatcher.createView(
+        ident,
+        "comment",
+        new Column[0],
+        representations,
+        null,
+        null,
+        ImmutableMap.of("k1", "v1", "p1", "pv1"));
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TreeLockTestSupport.java 
b/core/src/test/java/org/apache/gravitino/catalog/TreeLockTestSupport.java
new file mode 100644
index 0000000000..160191f838
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/catalog/TreeLockTestSupport.java
@@ -0,0 +1,214 @@
+/*
+ * 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.gravitino.catalog;
+
+import java.time.Duration;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.lock.LockType;
+import org.apache.gravitino.lock.TreeLock;
+import org.apache.gravitino.lock.TreeLockNode;
+import org.junit.jupiter.api.Assertions;
+
+/**
+ * Helpers for asserting how a dispatcher operation interacts with tree locks 
held by other threads.
+ * A {@link HeldLock} pins a lock on a helper thread so a test can check 
whether an operation runs
+ * concurrently with it or waits for it. Waiting is detected by observing the 
operation thread
+ * parked inside {@link TreeLockNode}, not by sleeping, so the assertions do 
not depend on timing.
+ */
+final class TreeLockTestSupport {
+
+  private static final Duration TIMEOUT = Duration.ofSeconds(30);
+
+  private TreeLockTestSupport() {}
+
+  /**
+   * Asserts that {@code operation} completes while {@code held} is still 
held, i.e. the operation
+   * does not contend with that lock.
+   */
+  static void assertRunsConcurrentlyWith(HeldLock held, Runnable operation) 
throws Exception {
+    OperationRun run = OperationRun.start(operation);
+    try {
+      if (run.awaitDoneOrParked() == Outcome.PARKED) {
+        held.release();
+        Assertions.fail(
+            "Operation parked on a tree lock while " + held + " was held; it 
contends with it");
+      }
+      run.join();
+    } finally {
+      run.shutdown();
+    }
+  }
+
+  /**
+   * Asserts that {@code operation} parks on a tree lock while {@code held} is 
held and completes
+   * once the lock is released.
+   */
+  static void assertWaitsFor(HeldLock held, Runnable operation) throws 
Exception {
+    OperationRun run = OperationRun.start(operation);
+    try {
+      Assertions.assertEquals(
+          Outcome.PARKED,
+          run.awaitDoneOrParked(),
+          "Operation finished without waiting for " + held);
+      held.release();
+      run.join();
+    } finally {
+      run.shutdown();
+    }
+  }
+
+  private enum Outcome {
+    DONE,
+    PARKED
+  }
+
+  /** An operation running on its own thread whose progress can be observed. */
+  private static final class OperationRun {
+    private final ExecutorService executor;
+    private final Thread[] worker = new Thread[1];
+    private Future<?> future;
+
+    private OperationRun() {
+      this.executor =
+          Executors.newSingleThreadExecutor(
+              r -> {
+                worker[0] = new Thread(r, "tree-lock-test-operation");
+                return worker[0];
+              });
+    }
+
+    static OperationRun start(Runnable operation) {
+      OperationRun run = new OperationRun();
+      run.future = run.executor.submit(operation);
+      return run;
+    }
+
+    /** Waits until the operation either completes or blocks inside {@link 
TreeLockNode#lock}. */
+    Outcome awaitDoneOrParked() throws Exception {
+      long deadline = System.nanoTime() + TIMEOUT.toNanos();
+      while (System.nanoTime() < deadline) {
+        if (future.isDone()) {
+          return Outcome.DONE;
+        }
+        Thread thread = worker[0];
+        if (thread != null && isParkedOnTreeLock(thread)) {
+          return Outcome.PARKED;
+        }
+        Thread.sleep(5);
+      }
+      throw new TimeoutException("Operation neither finished nor parked on a 
tree lock");
+    }
+
+    void join() throws Exception {
+      try {
+        future.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+      } catch (ExecutionException e) {
+        throw new AssertionError("Operation failed", e.getCause());
+      }
+    }
+
+    void shutdown() {
+      executor.shutdownNow();
+    }
+
+    private static boolean isParkedOnTreeLock(Thread thread) {
+      Thread.State state = thread.getState();
+      if (state != Thread.State.WAITING && state != 
Thread.State.TIMED_WAITING) {
+        return false;
+      }
+      for (StackTraceElement frame : thread.getStackTrace()) {
+        if (TreeLockNode.class.getName().equals(frame.getClassName())
+            && "lock".equals(frame.getMethodName())) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  /** A tree lock held by a dedicated thread until {@link #release()} or 
{@link #close()}. */
+  static final class HeldLock implements AutoCloseable {
+    private final NameIdentifier identifier;
+    private final LockType lockType;
+    private final CountDownLatch acquired = new CountDownLatch(1);
+    private final CountDownLatch releaseSignal = new CountDownLatch(1);
+    private final Thread holder;
+    private volatile Throwable failure;
+
+    private HeldLock(NameIdentifier identifier, LockType lockType) {
+      this.identifier = identifier;
+      this.lockType = lockType;
+      this.holder = new Thread(this::hold, "tree-lock-test-holder");
+    }
+
+    /** Acquires the lock on a helper thread and returns once it is held. */
+    static HeldLock acquire(NameIdentifier identifier, LockType lockType) 
throws Exception {
+      HeldLock held = new HeldLock(identifier, lockType);
+      held.holder.start();
+      held.acquired.await(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+      if (held.failure != null) {
+        throw new AssertionError("Failed to acquire " + held, held.failure);
+      }
+      return held;
+    }
+
+    private void hold() {
+      TreeLock lock = 
GravitinoEnv.getInstance().lockManager().createTreeLock(identifier);
+      try {
+        lock.lock(lockType);
+      } catch (Throwable t) {
+        failure = t;
+        acquired.countDown();
+        return;
+      }
+      acquired.countDown();
+      try {
+        releaseSignal.await();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      } finally {
+        lock.unlock();
+      }
+    }
+
+    /** Releases the lock and waits for the holder thread to exit. */
+    void release() throws InterruptedException {
+      releaseSignal.countDown();
+      holder.join(TIMEOUT.toMillis());
+    }
+
+    @Override
+    public void close() throws InterruptedException {
+      release();
+    }
+
+    @Override
+    public String toString() {
+      return lockType + " lock on " + identifier;
+    }
+  }
+}

Reply via email to