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

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new fb7b7fa748 [CALCITE-7872] Concurrent class initialization of 
RelCollations and RelCollationImpl should not deadlock
fb7b7fa748 is described below

commit fb7b7fa74822b4f6835803eab9d0a5d304860a98
Author: Alan Jin <[email protected]>
AuthorDate: Wed Oct 21 07:06:46 2026 +0000

    [CALCITE-7872] Concurrent class initialization of RelCollations and 
RelCollationImpl should not deadlock
---
 .../org/apache/calcite/rel/RelCollationImpl.java   |  25 ++++-
 .../java/org/apache/calcite/rel/RelCollations.java |   9 +-
 .../org/apache/calcite/rel/RelCollationTest.java   | 115 +++++++++++++++++++++
 3 files changed, 138 insertions(+), 11 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/rel/RelCollationImpl.java 
b/core/src/main/java/org/apache/calcite/rel/RelCollationImpl.java
index 19219f741a..45d231a36e 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelCollationImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelCollationImpl.java
@@ -42,11 +42,26 @@
 public class RelCollationImpl implements RelCollation {
   //~ Static fields/initializers ---------------------------------------------
 
+  // The static initializer of RelCollations creates instances of this class,
+  // so the static initializer of this class, including the constructor that
+  // it calls, must not use RelCollations; otherwise two threads that
+  // initialize the two classes at the same time can deadlock.
+  // RelCollations.EMPTY is canonized too, so it is the same object as EMPTY.
+
   @Deprecated // to be removed before 2.0
-  public static final RelCollation EMPTY = RelCollations.EMPTY;
+  public static final RelCollation EMPTY =
+      RelCollationTraitDef.INSTANCE.canonize(
+          new RelCollationImpl(ImmutableList.of()));
 
   @Deprecated // to be removed before 2.0
-  public static final RelCollation PRESERVE = RelCollations.PRESERVE;
+  public static final RelCollation PRESERVE =
+      RelCollationTraitDef.INSTANCE.canonize(
+          new RelCollationImpl(
+              ImmutableList.of(new RelFieldCollation(-1))) {
+            @Override public String toString() {
+              return "PRESERVE";
+            }
+          });
 
   //~ Instance fields --------------------------------------------------------
 
@@ -56,7 +71,11 @@ public class RelCollationImpl implements RelCollation {
 
   protected RelCollationImpl(ImmutableList<RelFieldCollation> fieldCollations) 
{
     this.fieldCollations = fieldCollations;
-    checkArgument(Util.isDistinct(RelCollations.ordinals(fieldCollations)),
+    // Not RelCollations.ordinals: this constructor runs in the static
+    // initializer of this class, which must not use RelCollations (see EMPTY).
+    checkArgument(
+        Util.isDistinct(
+            Util.transform(fieldCollations, RelFieldCollation::getFieldIndex)),
         "fields must be distinct");
   }
 
diff --git a/core/src/main/java/org/apache/calcite/rel/RelCollations.java 
b/core/src/main/java/org/apache/calcite/rel/RelCollations.java
index e5107e4206..16f4cddf51 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelCollations.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelCollations.java
@@ -51,14 +51,7 @@ public class RelCollations {
    * implementation choice is to apply operations that preserve order.
    */
   @Deprecated // to be removed before 2.0
-  public static final RelCollation PRESERVE =
-      RelCollationTraitDef.INSTANCE.canonize(
-          new RelCollationImpl(
-              ImmutableList.of(new RelFieldCollation(-1))) {
-            @Override public String toString() {
-              return "PRESERVE";
-            }
-          });
+  public static final RelCollation PRESERVE = RelCollationImpl.PRESERVE;
 
   private RelCollations() {}
 
diff --git a/core/src/test/java/org/apache/calcite/rel/RelCollationTest.java 
b/core/src/test/java/org/apache/calcite/rel/RelCollationTest.java
index 58bf0b6374..9d4b51d60a 100644
--- a/core/src/test/java/org/apache/calcite/rel/RelCollationTest.java
+++ b/core/src/test/java/org/apache/calcite/rel/RelCollationTest.java
@@ -21,12 +21,18 @@
 import org.apache.calcite.util.mapping.Mappings;
 
 import com.google.common.collect.Lists;
+import com.google.common.io.ByteStreams;
 
 import org.junit.jupiter.api.Test;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
 
 import static org.apache.calcite.rel.RelCollations.EMPTY;
 import static org.apache.calcite.rel.RelFieldCollation.Direction.ASCENDING;
@@ -37,6 +43,8 @@
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 /**
@@ -220,6 +228,71 @@ class RelCollationTest {
     assertThat(CLUSTERED.reverse(), is(CLUSTERED));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7872";>[CALCITE-7872]
+   * RelCollations and RelCollationImpl can deadlock during class
+   * initialization</a>.
+   *
+   * <p>Tests that the deprecated constants {@code RelCollationImpl.EMPTY} and
+   * {@code RelCollationImpl.PRESERVE} are the same objects as
+   * {@link RelCollations#EMPTY} and {@link RelCollations#PRESERVE}, whichever
+   * of the two classes is initialized first. They used to be null if
+   * {@code RelCollations} was initialized first. */
+  @Test void testDeprecatedConstantsInEitherInitializationOrder()
+      throws Exception {
+    final List<Class<?>> classes =
+        Arrays.asList(RelCollations.class, RelCollationImpl.class);
+    for (List<Class<?>> order : Arrays.asList(classes, 
Lists.reverse(classes))) {
+      final ClassLoader loader = new IsolatingClassLoader();
+      for (Class<?> c : order) {
+        Class.forName(c.getName(), true, loader);
+      }
+      final Class<?> relCollations =
+          loader.loadClass(RelCollations.class.getName());
+      final Class<?> relCollationImpl =
+          loader.loadClass(RelCollationImpl.class.getName());
+      for (String name : Arrays.asList("EMPTY", "PRESERVE")) {
+        final Object expected = relCollations.getField(name).get(null);
+        assertThat(expected, notNullValue());
+        assertThat(relCollationImpl.getField(name).get(null),
+            sameInstance(expected));
+      }
+    }
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7872";>[CALCITE-7872]
+   * RelCollations and RelCollationImpl can deadlock during class
+   * initialization</a>.
+   *
+   * <p>Tests that two threads can initialize {@link RelCollations} and
+   * {@link RelCollationImpl} at the same time. The static initializers of the
+   * two classes used to depend on each other, so the threads deadlocked. */
+  @Test void testConcurrentClassInitialization() throws Exception {
+    for (int i = 0; i < 5; i++) {
+      final ClassLoader loader = new IsolatingClassLoader();
+      final CyclicBarrier barrier = new CyclicBarrier(2);
+      final List<FutureTask<Class<?>>> tasks = new ArrayList<>();
+      for (Class<?> c
+          : Arrays.asList(RelCollations.class, RelCollationImpl.class)) {
+        final FutureTask<Class<?>> task =
+            new FutureTask<>(() -> {
+              barrier.await();
+              return Class.forName(c.getName(), true, loader);
+            });
+        final Thread thread = new Thread(task);
+        // A thread stuck in a static initializer cannot be stopped
+        thread.setDaemon(true);
+        thread.start();
+        tasks.add(task);
+      }
+      for (FutureTask<Class<?>> task : tasks) {
+        // Throws TimeoutException if the threads deadlock
+        assertThat(task.get(10, TimeUnit.SECONDS), notNullValue());
+      }
+    }
+  }
+
   private static RelCollation collation(int... ordinals) {
     final List<RelFieldCollation> list = new ArrayList<>();
     for (int ordinal : ordinals) {
@@ -231,4 +304,46 @@ private static RelCollation collation(int... ordinals) {
   private static Mapping mapping(int sourceCount, int... sources) {
     return Mappings.target(ImmutableIntList.of(sources), sourceCount);
   }
+
+  /** Class loader that defines its own copy of each Calcite class, so that the
+   * static initializers of those classes run again, and delegates other
+   * classes to its parent. */
+  private static class IsolatingClassLoader extends ClassLoader {
+    private final ClassLoader parent;
+
+    IsolatingClassLoader() {
+      this(RelCollationTest.class.getClassLoader());
+    }
+
+    private IsolatingClassLoader(ClassLoader parent) {
+      super(parent);
+      this.parent = parent;
+    }
+
+    @Override protected Class<?> loadClass(String name, boolean resolve)
+        throws ClassNotFoundException {
+      if (!name.startsWith("org.apache.calcite.")) {
+        return super.loadClass(name, resolve);
+      }
+      synchronized (getClassLoadingLock(name)) {
+        Class<?> c = findLoadedClass(name);
+        if (c == null) {
+          final String resource = name.replace('.', '/') + ".class";
+          try (InputStream in = parent.getResourceAsStream(resource)) {
+            if (in == null) {
+              throw new ClassNotFoundException(name);
+            }
+            final byte[] bytes = ByteStreams.toByteArray(in);
+            c = defineClass(name, bytes, 0, bytes.length);
+          } catch (IOException e) {
+            throw new ClassNotFoundException(name, e);
+          }
+        }
+        if (resolve) {
+          resolveClass(c);
+        }
+        return c;
+      }
+    }
+  }
 }

Reply via email to