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

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


The following commit(s) were added to refs/heads/main by this push:
     new 34dcfa1fa8 [#9084] improvement(core): Fix race condition in 
AuthorizationRequestContext.loadRole() (#9086)
34dcfa1fa8 is described below

commit 34dcfa1fa879e2f25e5e346b382b50ea26b4f6d4
Author: Harsh Mehta <[email protected]>
AuthorDate: Fri Dec 5 14:23:29 2025 +0530

    [#9084] improvement(core): Fix race condition in 
AuthorizationRequestContext.loadRole() (#9086)
    
    Fixed #9084
    
    - Replaced separate `hasLoadRole.get()` and `hasLoadRole.set(true)`
    calls with an atomic
      `hasLoadRole.compareAndSet(false, true)` operation.
    - This makes the check-and-set atomic, ensuring the runnable executes
    only once,
      even under concurrent invocations.
    - Previously, multiple threads could see `false` and execute the
    runnable simultaneously,
      causing duplicate executions.
    
    - Added a new unit test:
    `testLoadRoleRunsOnceEvenWhenInvokedConcurrently`.
    - Verified that the runnable executes only once, even when `loadRole()`
    is called concurrently.
    
    ### Result
    Improved thread-safety of `loadRole()` and prevented race conditions
    during concurrent access.
    
    ---------
    
    Signed-off-by: harsh-mehta-sb <[email protected]>
    Signed-off-by: Harsh Mehta <[email protected]>
---
 .../authorization/AuthorizationRequestContext.java |  13 ++-
 .../TestAuthorizationRequestContext.java           | 104 +++++++++++++++++++++
 2 files changed, 115 insertions(+), 2 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
 
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
index bc8964ccfa..392eb13c27 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java
@@ -82,8 +82,17 @@ public class AuthorizationRequestContext {
     if (hasLoadRole.get()) {
       return;
     }
-    runnable.run();
-    hasLoadRole.set(true);
+    synchronized (this) {
+      if (hasLoadRole.get()) {
+        return;
+      }
+      try {
+        runnable.run();
+        hasLoadRole.set(true);
+      } catch (Exception e) {
+        throw new RuntimeException("Failed to load role: ", e);
+      }
+    }
   }
 
   public String getOriginalAuthorizationExpression() {
diff --git 
a/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java
 
b/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java
new file mode 100644
index 0000000000..2216a7dcfe
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java
@@ -0,0 +1,104 @@
+/*
+ * 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.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+
+public class TestAuthorizationRequestContext {
+
+  @Test
+  public void testLoadRoleRunsOnceEvenWhenInvokedConcurrently() throws 
Exception {
+    AuthorizationRequestContext context = new AuthorizationRequestContext();
+    AtomicInteger counter = new AtomicInteger();
+    CountDownLatch firstStarted = new CountDownLatch(1);
+    CountDownLatch allowFinish = new CountDownLatch(1);
+
+    Thread firstInvocation =
+        new Thread(
+            () ->
+                context.loadRole(
+                    () -> {
+                      counter.incrementAndGet();
+                      firstStarted.countDown();
+                      try {
+                        allowFinish.await(5, TimeUnit.SECONDS);
+                      } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                      }
+                    }));
+    firstInvocation.start();
+
+    try {
+      assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
+      context.loadRole(counter::incrementAndGet);
+      assertEquals(1, counter.get());
+    } finally {
+      allowFinish.countDown();
+      firstInvocation.join();
+    }
+
+    context.loadRole(counter::incrementAndGet);
+    assertEquals(1, counter.get(), "Subsequent loadRole calls should be 
ignored");
+  }
+
+  @Test
+  public void testLoadRoleFailThenSuccessThenIgnored() throws Exception {
+    AuthorizationRequestContext context = new AuthorizationRequestContext();
+    AtomicInteger counter = new AtomicInteger();
+
+    CountDownLatch failingStarted = new CountDownLatch(1);
+    CountDownLatch allowFailToThrow = new CountDownLatch(1);
+
+    Thread failingThread =
+        new Thread(
+            () -> {
+              try {
+                context.loadRole(
+                    () -> {
+                      counter.incrementAndGet();
+                      failingStarted.countDown();
+                      try {
+                        allowFailToThrow.await(2, TimeUnit.SECONDS);
+                      } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                      }
+                      throw new IllegalStateException("Simulated failure");
+                    });
+              } catch (RuntimeException e) {
+                assertTrue(e.getMessage().contains("Failed to load role"));
+                assertInstanceOf(IllegalStateException.class, e.getCause());
+              }
+            });
+
+    failingThread.start();
+    assertTrue(failingStarted.await(2, TimeUnit.SECONDS));
+    allowFailToThrow.countDown();
+    failingThread.join();
+    context.loadRole(counter::incrementAndGet);
+    assertEquals(2, counter.get(), "Flag should remain false after failure so 
next call runs.");
+    context.loadRole(counter::incrementAndGet);
+    assertEquals(2, counter.get(), "After a successful loadRole, further calls 
must be ignored.");
+  }
+}

Reply via email to