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

yuqi1129 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 7b6cf6b58c [#12475] improvement(common): make CallerContext an 
immutable value object (#12476)
7b6cf6b58c is described below

commit 7b6cf6b58c314216c8f815c18c4177b0e00d238a
Author: YangJie <[email protected]>
AuthorDate: Tue Aug 18 21:48:59 2026 +0800

    [#12475] improvement(common): make CallerContext an immutable value object 
(#12476)
    
    ### What changes are proposed in this pull request?
    
    Make `CallerContext` a truly immutable value object:
    - `withContext` now stores a defensive unmodifiable copy
    (`Collections.unmodifiableMap(new HashMap<>(context))`) instead of the
    caller's map by reference.
    - The `context` field is now `final`, assigned once via a private
    constructor; the `Builder` accumulates the map and `build()` returns a
    distinct `new CallerContext(...)`.
    
    `CallerContext` is published into a `ThreadLocal` and read across
    modules (server fileset/credential ops, event dispatcher, GVFS clients).
    It is forked from Hadoop's `CallerContext`, which is fully immutable,
    but the Gravitino copy had lost that: the map was exposed by reference
    and a retained `Builder` could mutate an already-built instance.
    
    ### Why are the changes needed?
    
    Defense-in-depth for an audit/credential context: prevent a caller's
    later map mutation or a reader's `put`/`remove` from silently corrupting
    the published context, and restore parity with the immutable Hadoop
    origin. No live bug today (all call sites pass fresh maps and only
    read), but a latent footgun on a shared, ThreadLocal-published object.
    
    Fix: #12475
    
    ### Does this PR introduce _any_ user-facing change?
    
    `context()` now returns an unmodifiable map. Within Gravitino nothing
    breaks (all readers are read-only). External GVFS/client code that
    mutated the returned map would now get `UnsupportedOperationException`.
    
    ### How was this patch tested?
    
    Extended `TestCallerContext` (1 → 7 tests): defensive copy,
    unmodifiability, null-rejection contract, empty map, builder-reuse
    isolation, equals/hashCode across a mutated source map. `./gradlew
    :common:test --tests "*.audit.TestCallerContext"` passes 7/7.
---
 .../org/apache/gravitino/audit/CallerContext.java  | 26 ++++----
 .../apache/gravitino/audit/TestCallerContext.java  | 70 ++++++++++++++++++++++
 2 files changed, 85 insertions(+), 11 deletions(-)

diff --git a/common/src/main/java/org/apache/gravitino/audit/CallerContext.java 
b/common/src/main/java/org/apache/gravitino/audit/CallerContext.java
index 0ae2250d07..d1324f6014 100644
--- a/common/src/main/java/org/apache/gravitino/audit/CallerContext.java
+++ b/common/src/main/java/org/apache/gravitino/audit/CallerContext.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.audit;
 
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -31,12 +33,14 @@ import java.util.Map;
  * 
<p>hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/CallerContext.java
  */
 public class CallerContext {
-  private Map<String, String> context;
+  private final Map<String, String> context;
 
-  private CallerContext() {}
+  private CallerContext(Map<String, String> context) {
+    this.context = context;
+  }
 
   /**
-   * Returns the context map in the caller context.
+   * Returns the context map in the caller context. The returned map is 
unmodifiable.
    *
    * @return the context map
    */
@@ -59,26 +63,26 @@ public class CallerContext {
 
   /** Builder to create a caller context. */
   public static class Builder {
-    private final CallerContext callerContext;
+    private Map<String, String> context;
 
-    private Builder() {
-      callerContext = new CallerContext();
-    }
+    private Builder() {}
 
     /**
-     * Sets the context for CallerContext
+     * Sets the context for CallerContext. The provided map is defensively 
copied into an
+     * unmodifiable map, so later mutations to the caller's map do not affect 
this context and the
+     * map returned by {@link CallerContext#context()} cannot be modified.
      *
      * @param context The context to set.
      * @return This Builder instance for method chaining.
      */
     public CallerContext.Builder withContext(Map<String, String> context) {
-      callerContext.context = context;
+      this.context = context == null ? null : Collections.unmodifiableMap(new 
HashMap<>(context));
       return this;
     }
 
     /** Validate the variables in the CallerContext. */
     private void validate() {
-      Preconditions.checkArgument(callerContext.context != null, "context 
cannot be null");
+      Preconditions.checkArgument(context != null, "context cannot be null");
     }
 
     /**
@@ -88,7 +92,7 @@ public class CallerContext {
      */
     public CallerContext build() {
       validate();
-      return callerContext;
+      return new CallerContext(context);
     }
   }
 
diff --git 
a/common/src/test/java/org/apache/gravitino/audit/TestCallerContext.java 
b/common/src/test/java/org/apache/gravitino/audit/TestCallerContext.java
index e0f3d4d56c..a350d11a94 100644
--- a/common/src/test/java/org/apache/gravitino/audit/TestCallerContext.java
+++ b/common/src/test/java/org/apache/gravitino/audit/TestCallerContext.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.audit;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import org.junit.jupiter.api.Assertions;
@@ -42,4 +43,73 @@ public class TestCallerContext {
       CallerContext.CallerContextHolder.remove();
     }
   }
+
+  @Test
+  public void testContextIsDefensivelyCopied() {
+    Map<String, String> contextMap = new HashMap<>();
+    contextMap.put("test", "test");
+    CallerContext callerContext = 
CallerContext.builder().withContext(contextMap).build();
+
+    // Mutating the original map after build must not affect the stored 
context.
+    contextMap.put("added-after-build", "value");
+    contextMap.remove("test");
+
+    Assertions.assertEquals(1, callerContext.context().size());
+    Assertions.assertEquals("test", callerContext.context().get("test"));
+    
Assertions.assertFalse(callerContext.context().containsKey("added-after-build"));
+  }
+
+  @Test
+  public void testContextIsUnmodifiable() {
+    Map<String, String> contextMap = new HashMap<>();
+    contextMap.put("test", "test");
+    CallerContext callerContext = 
CallerContext.builder().withContext(contextMap).build();
+
+    Assertions.assertThrows(
+        UnsupportedOperationException.class, () -> 
callerContext.context().put("k", "v"));
+  }
+
+  @Test
+  public void testNullContextStillRejectedOnBuild() {
+    CallerContext.Builder builder = CallerContext.builder().withContext(null);
+    Assertions.assertThrows(IllegalArgumentException.class, builder::build);
+  }
+
+  @Test
+  public void testEmptyContextIsAllowed() {
+    CallerContext callerContext = CallerContext.builder().withContext(new 
HashMap<>()).build();
+    Assertions.assertTrue(callerContext.context().isEmpty());
+  }
+
+  @Test
+  public void testReusingBuilderDoesNotMutateAlreadyBuiltContext() {
+    Map<String, String> first = new HashMap<>();
+    first.put("k", "v1");
+    CallerContext.Builder builder = CallerContext.builder().withContext(first);
+    CallerContext built = builder.build();
+
+    // Reusing the same builder to set a different context must not affect the 
already-built
+    // instance, which is now an independent immutable value object.
+    Map<String, String> second = new HashMap<>();
+    second.put("k", "v2");
+    builder.withContext(second);
+
+    Assertions.assertEquals("v1", built.context().get("k"));
+  }
+
+  @Test
+  public void testContextEqualsAcrossMutatedSourceMap() {
+    Map<String, String> source = new HashMap<>();
+    source.put("k", "v");
+    CallerContext expected = 
CallerContext.builder().withContext(source).build();
+
+    // Mutating the source after building must not change equality against a 
context built from the
+    // original contents.
+    source.put("k2", "v2");
+    CallerContext actual =
+        CallerContext.builder().withContext(Collections.singletonMap("k", 
"v")).build();
+
+    Assertions.assertEquals(expected, actual);
+    Assertions.assertEquals(expected.hashCode(), actual.hashCode());
+  }
 }

Reply via email to