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

szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git


The following commit(s) were added to refs/heads/master by this push:
     new 9fe354c60 RATIS-2510. NettyClientStreamRpc may retain a completely 
released object. (#1456)
9fe354c60 is described below

commit 9fe354c60d2f4820c05fc31dd31a71b0986ff96b
Author: Tsz-Wo Nicholas Sze <[email protected]>
AuthorDate: Sun May 10 08:50:06 2026 -0700

    RATIS-2510. NettyClientStreamRpc may retain a completely released object. 
(#1456)
---
 .../apache/ratis/util/ReferenceCountedObject.java  | 233 ++++++++++++++++++---
 .../ratis/netty/client/NettyClientStreamRpc.java   |  46 ++--
 .../ratis/netty/server/DataStreamManagement.java   |   7 +-
 .../ratis/util/TestReferenceCountedObject.java     |  73 +++++--
 4 files changed, 292 insertions(+), 67 deletions(-)

diff --git 
a/ratis-common/src/main/java/org/apache/ratis/util/ReferenceCountedObject.java 
b/ratis-common/src/main/java/org/apache/ratis/util/ReferenceCountedObject.java
index 0dd378dc0..ffcbf51bd 100644
--- 
a/ratis-common/src/main/java/org/apache/ratis/util/ReferenceCountedObject.java
+++ 
b/ratis-common/src/main/java/org/apache/ratis/util/ReferenceCountedObject.java
@@ -23,6 +23,7 @@ import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 /**
  * A reference-counted object can be retained for later use
@@ -39,12 +40,19 @@ import java.util.function.Consumer;
  *   it must be released the same number of times.
  * <p>
  * - If the object has been retained and then completely released (i.e. the 
reference count becomes 0),
- *   it must not be retained/released/accessed anymore since it may have been 
allocated for other use.
+ *   calling {@link #get()} will lead to an {@link IllegalStateException}.
+ *   Depending on how the {@link ReferenceCountedObject} is built,
+ *   calling {@link #retain()} may or may not be allowed;
+ * @see Builder#setValue(Object)
+ * @see Builder#setConstructor(Supplier)
  *
  * @param <T> The object type.
  */
 public interface ReferenceCountedObject<T> {
-  /** @return the object. */
+  /**
+   * @return the object.
+   * @throws IllegalStateException when the object has not been retained.
+   */
   T get();
 
   /**
@@ -74,7 +82,7 @@ public interface ReferenceCountedObject<T> {
       @Override
       public T get() {
         if (closed.get()) {
-          throw new IllegalStateException("Already closed");
+          throw new IllegalStateException("Failed to get: already closed");
         }
         return retained;
       }
@@ -96,36 +104,148 @@ public interface ReferenceCountedObject<T> {
    */
   boolean release();
 
-  /** The same as wrap(value, EMPTY, EMPTY), where EMPTY is an empty method. */
+  /**
+   * The same as newBuilder().setValue(value).build();
+   *
+   * @deprecated use {@link Builder}
+   */
+  @Deprecated
   static <V> ReferenceCountedObject<V> wrap(V value) {
-    return wrap(value, () -> {}, ignored -> {});
+    return ReferenceCountedObject.<V>newBuilder().setValue(value).build();
   }
 
   /**
-   * Wrap the given value as a {@link ReferenceCountedObject}.
+   * The same as newBuilder()
+   *   .setValue(value)
+   *   .setRetainMethod(retainMethod)
+   *   .setBooleanReleaseMethod(releaseMethod)
+   *   .build();
    *
-   * @param value the value being wrapped.
-   * @param retainMethod a method to run when {@link #retain()} is invoked.
-   * @param releaseMethod a method to run when {@link #release()} is invoked,
-   *                      where the method takes a boolean which is the same 
as the one returned by {@link #release()}.
-   * @param <V> The value type.
-   * @return the wrapped reference-counted object.
+   * @deprecated use {@link Builder}
    */
+  @Deprecated
   static <V> ReferenceCountedObject<V> wrap(V value, Runnable retainMethod, 
Consumer<Boolean> releaseMethod) {
-    Objects.requireNonNull(value, "value == null");
-    Objects.requireNonNull(retainMethod, "retainMethod == null");
-    Objects.requireNonNull(releaseMethod, "releaseMethod == null");
+    return ReferenceCountedObject.<V>newBuilder()
+        .setValue(value)
+        .setRetainMethod(retainMethod)
+        .setBooleanReleaseMethod(releaseMethod)
+        .build();
+  }
+
+  static <V> Builder<V> newBuilder() {
+    return new Builder<>();
+  }
+
+  /**
+   * To build {@link ReferenceCountedObject},
+   * where it may use either a fixed value or a constructor (but not both).
+   * @see Builder#setValue(Object)
+   * @see Builder#setConstructor(Supplier)
+   *
+   * @param <V> The type of the {@link ReferenceCountedObject} being built.
+   */
+  class Builder<V> {
+    private V value = null;
+    private Supplier<V> constructor = null;
+    private Runnable retainMethod = () -> {};
+    private Consumer<V> releaseMethod = v -> {};
+
+    /**
+     * Set a fixed value for the {@link ReferenceCountedObject} being built.
+     * Once it has been completely released, calling {@link #retain()} is not 
allowed.
+     *
+     * @param value a fixed value.
+     */
+    public Builder<V> setValue(V value) {
+      this.value = value;
+      return this;
+    }
+
+    /**
+     * Set a constructor for the {@link ReferenceCountedObject} being built.
+     * The value is constructed at the first {@link #retain()} call,
+     * After it has been completely released by {@link #release()},
+     * a new value will be constructed when {@link #retain()} is called again.
+     *
+     * @param constructor to construct the object.
+     */
+    public Builder<V> setConstructor(Supplier<V> constructor) {
+      this.constructor = constructor;
+      return this;
+    }
+
+    /**
+     * @param retainMethod a method to run when {@link #retain()} is invoked.
+     */
+    public Builder<V> setRetainMethod(Runnable retainMethod) {
+      this.retainMethod = retainMethod != null ? retainMethod : () -> {};
+      return this;
+    }
+
+    /**
+     * @param releaseMethod a method to run when {@link #release()} is invoked,
+     *                      where the method has a parameter,
+     *                      where the actual parameter is the value when it is 
completely released;
+     *                            otherwise, the actual parameter is null.
+     *                      The method may clean up the value when it is 
completely released.
+     */
+    public Builder<V> setReleaseMethod(Consumer<V> releaseMethod) {
+      this.releaseMethod = releaseMethod != null ? releaseMethod : ignored -> 
{};
+      return this;
+    }
 
-    return new ReferenceCountedObject<V>() {
+    /**
+     * @param booleanReleaseMethod a method to run when {@link #release()} is 
invoked,
+     *                      where the method takes the boolean returned from 
{@link #release()}.
+     */
+    public Builder<V> setBooleanReleaseMethod(Consumer<Boolean> 
booleanReleaseMethod) {
+      this.releaseMethod = booleanReleaseMethod != null ? v -> 
booleanReleaseMethod.accept(v != null) : ignored -> {};
+      return this;
+    }
+
+    /**
+     * @param releaseMethod a method to run when {@link #release()} is invoked,
+     */
+    public Builder<V> setReleaseMethod(Runnable releaseMethod) {
+      this.releaseMethod = releaseMethod != null ? ignored -> 
releaseMethod.run() : ignored -> {};
+      return this;
+    }
+
+    public ReferenceCountedObject<V> build() {
+      if (value == null) {
+        Objects.requireNonNull(constructor, "Both value == null and 
constructor == null");
+        return new ConstructorWrapper<>(constructor, retainMethod, 
releaseMethod);
+      } else {
+        Preconditions.assertNull(constructor, "Both value != null and 
constructor != null");
+        return new ValueWrapper<>(value, retainMethod, releaseMethod);
+      }
+    }
+
+    /**
+     * Wrap a fixed value as a {@link ReferenceCountedObject}.
+     * When it is completely released, it cannot be retained again.
+     *
+     * @param <V> The value type.
+     */
+    private static final class ValueWrapper<V> implements 
ReferenceCountedObject<V> {
+      private final V value;
+      private final Runnable retainMethod;
+      private final Consumer<V> releaseMethod;
       private final AtomicInteger count = new AtomicInteger();
 
+      private ValueWrapper(V value, Runnable retainMethod, Consumer<V> 
releaseMethod) {
+        this.value = Objects.requireNonNull(value, "value == null");
+        this.retainMethod = Objects.requireNonNull(retainMethod, "retainMethod 
== null");
+        this.releaseMethod = Objects.requireNonNull(releaseMethod, 
"releaseMethod == null");
+      }
+
       @Override
       public V get() {
         final int previous = count.get();
         if (previous < 0) {
-          throw new IllegalStateException("Failed to get: object has already 
been completely released.");
+          throw new IllegalStateException("Failed to get: already completely 
released.");
         } else if (previous == 0) {
-          throw new IllegalStateException("Failed to get: object has not yet 
been retained.");
+          throw new IllegalStateException("Failed to get: not yet retained.");
         }
         return value;
       }
@@ -135,7 +255,7 @@ public interface ReferenceCountedObject<T> {
         // n <  0: exception
         // n >= 0: n++
         if (count.getAndUpdate(n -> n < 0? n : n + 1) < 0) {
-          throw new IllegalStateException("Failed to retain: object has 
already been completely released.");
+          throw new IllegalStateException("Failed to retain: already 
completely released.");
         }
 
         retainMethod.run();
@@ -149,19 +269,78 @@ public interface ReferenceCountedObject<T> {
         // n == 1: n = -1
         final int previous = count.getAndUpdate(n -> n <= 1? -1: n - 1);
         if (previous < 0) {
-          throw new IllegalStateException("Failed to release: object has 
already been completely released.");
+          throw new IllegalStateException("Failed to release: already 
completely released.");
         } else if (previous == 0) {
-          throw new IllegalStateException("Failed to release: object has not 
yet been retained.");
+          throw new IllegalStateException("Failed to release: not yet 
retained.");
         }
-        final boolean completedReleased = previous == 1;
-        releaseMethod.accept(completedReleased);
-        return completedReleased;
+        final boolean completelyReleased = previous == 1;
+        releaseMethod.accept(completelyReleased ? value : null);
+        return completelyReleased;
       }
-    };
+    }
+
+    /**
+     * Wrap a constructor as a {@link ReferenceCountedObject}.
+     *
+     * @see Builder#setConstructor(Supplier)
+     */
+    private static final class ConstructorWrapper<V> implements 
ReferenceCountedObject<V> {
+      private final Supplier<V> constructor;
+      private final Runnable retainMethod;
+      private final Consumer<V> releaseMethod;
+      private ValueWrapper<V> valueWrapper;
+
+      private ConstructorWrapper(Supplier<V> constructor, Runnable 
retainMethod, Consumer<V> releaseMethod) {
+        this.constructor = Objects.requireNonNull(constructor, "constructor == 
null");
+        this.retainMethod = Objects.requireNonNull(retainMethod, "retainMethod 
== null");
+        this.releaseMethod = Objects.requireNonNull(releaseMethod, 
"releaseMethod == null");
+      }
+
+      @Override
+      public synchronized V get() {
+        if (valueWrapper == null) {
+          throw new IllegalStateException("Failed to get: not yet retained.");
+        }
+        return valueWrapper.get();
+      }
+
+      @Override
+      public synchronized V retain() {
+        if (valueWrapper == null) {
+          valueWrapper = new ValueWrapper<>(constructor.get(), retainMethod, 
releaseMethod);
+        }
+        return valueWrapper.retain();
+      }
+
+      @Override
+      public synchronized boolean release() {
+        if (valueWrapper == null) {
+          throw new IllegalStateException("Failed to release: not yet 
retained.");
+        }
+        if (valueWrapper.release()) {
+          valueWrapper = null;
+          return true;
+        }
+        return false;
+      }
+    }
   }
 
-  /** The same as wrap(value, retainMethod, ignored -> releaseMethod.run()). */
+  /**
+   * The same as newBuilder()
+   *   .setValue(value)
+   *   .setRetainMethod(retainMethod)
+   *   .setReleaseMethod(releaseMethod)
+   *   .build();
+   *
+   * @deprecated use {@link Builder}
+   */
+  @Deprecated
   static <V> ReferenceCountedObject<V> wrap(V value, Runnable retainMethod, 
Runnable releaseMethod) {
-    return wrap(value, retainMethod, ignored -> releaseMethod.run());
+    return ReferenceCountedObject.<V>newBuilder()
+        .setValue(value)
+        .setRetainMethod(retainMethod)
+        .setReleaseMethod(releaseMethod)
+        .build();
   }
 }
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
 
b/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
index 4970d244a..2bfeea31e 100644
--- 
a/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/client/NettyClientStreamRpc.java
@@ -56,6 +56,7 @@ import 
org.apache.ratis.thirdparty.io.netty.handler.codec.ByteToMessageDecoder;
 import 
org.apache.ratis.thirdparty.io.netty.handler.codec.MessageToMessageEncoder;
 import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContext;
 import org.apache.ratis.util.JavaUtils;
+import org.apache.ratis.util.MemoizedFunction;
 import org.apache.ratis.util.MemoizedSupplier;
 import org.apache.ratis.util.NetUtils;
 import org.apache.ratis.util.Preconditions;
@@ -84,36 +85,30 @@ import static 
org.apache.ratis.datastream.impl.DataStreamPacketByteBuffer.EMPTY_
 public class NettyClientStreamRpc implements DataStreamClientRpc {
   public static final Logger LOG = 
LoggerFactory.getLogger(NettyClientStreamRpc.class);
 
-  private static class WorkerGroupGetter implements Supplier<EventLoopGroup> {
-
-    private static final 
AtomicReference<CompletableFuture<ReferenceCountedObject<EventLoopGroup>>> 
SHARED_WORKER_GROUP
-        = new AtomicReference<>();
+  private static class WorkerGroupGetter {
+    private static final MemoizedFunction<RaftProperties, 
ReferenceCountedObject<EventLoopGroup>> SHARED
+        = MemoizedFunction.valueOf(properties -> 
ReferenceCountedObject.<EventLoopGroup>newBuilder()
+          .setConstructor(() -> newWorkerGroup(properties))
+          .setReleaseMethod(eventLoopGroup -> {
+            if (eventLoopGroup != null) {
+              eventLoopGroup.shutdownGracefully();
+            }
+          })
+          .build());
 
     static WorkerGroupGetter newInstance(RaftProperties properties) {
       final boolean shared = 
NettyConfigKeys.DataStream.Client.workerGroupShare(properties);
-      if (shared) {
-        final CompletableFuture<ReferenceCountedObject<EventLoopGroup>> 
created = new CompletableFuture<>();
-        final CompletableFuture<ReferenceCountedObject<EventLoopGroup>> current
-            = SHARED_WORKER_GROUP.updateAndGet(g -> g != null ? g : created);
-        if (current == created) {
-          
created.complete(ReferenceCountedObject.wrap(newWorkerGroup(properties)));
-        }
-        return new WorkerGroupGetter(current.join().retain()) {
-          @Override
-          void shutdownGracefully() {
-            final CompletableFuture<ReferenceCountedObject<EventLoopGroup>> 
returned
-                = SHARED_WORKER_GROUP.updateAndGet(previous -> {
-              Preconditions.assertSame(current, previous, 
"SHARED_WORKER_GROUP");
-              return previous.join().release() ? null : previous;
-            });
-            if (returned == null) {
-              get().shutdownGracefully();
-            }
-          }
-        };
-      } else {
+      if (!shared) {
         return new WorkerGroupGetter(newWorkerGroup(properties));
       }
+
+      final ReferenceCountedObject<EventLoopGroup> ref = 
SHARED.apply(properties);
+      return new WorkerGroupGetter(ref.retain()) {
+        @Override
+        void shutdownGracefully() {
+          ref.release();
+        }
+      };
     }
 
     static EventLoopGroup newWorkerGroup(RaftProperties properties) {
@@ -129,7 +124,6 @@ public class NettyClientStreamRpc implements 
DataStreamClientRpc {
       this.workerGroup = workerGroup;
     }
 
-    @Override
     public final EventLoopGroup get() {
       return workerGroup;
     }
diff --git 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
index 0e10b0f4d..f3b98054d 100644
--- 
a/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
+++ 
b/ratis-netty/src/main/java/org/apache/ratis/netty/server/DataStreamManagement.java
@@ -311,8 +311,11 @@ public class DataStreamManagement {
       if (buffer.remaining() == 0) {
         continue;
       }
-      final ReferenceCountedObject<ByteBuffer> wrapped = 
ReferenceCountedObject.wrap(
-          buffer, buf::retain, ignored -> buf.release());
+      final ReferenceCountedObject<ByteBuffer> wrapped = 
ReferenceCountedObject.<ByteBuffer>newBuilder()
+          .setValue(buffer)
+          .setRetainMethod(buf::retain)
+          .setReleaseMethod(ignored -> buf.release())
+          .build();
       try(UncheckedAutoCloseable ignore = wrapped.retainAndReleaseOnClose()) {
         byteWritten += channel.write(wrapped);
       } catch (Throwable t) {
diff --git 
a/ratis-test/src/test/java/org/apache/ratis/util/TestReferenceCountedObject.java
 
b/ratis-test/src/test/java/org/apache/ratis/util/TestReferenceCountedObject.java
index 243631014..c1d7a3bfa 100644
--- 
a/ratis-test/src/test/java/org/apache/ratis/util/TestReferenceCountedObject.java
+++ 
b/ratis-test/src/test/java/org/apache/ratis/util/TestReferenceCountedObject.java
@@ -24,6 +24,8 @@ import org.junit.jupiter.api.Timeout;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
 public class TestReferenceCountedObject {
   static void assertValues(
       AtomicInteger retained, int expectedRetained,
@@ -35,20 +37,13 @@ public class TestReferenceCountedObject {
   static void assertRelease(ReferenceCountedObject<?> ref,
       AtomicInteger retained, int expectedRetained,
       AtomicInteger released, int expectedReleased) {
-    final boolean returned = ref.release();
+    final boolean completelyReleased = ref.release();
     assertValues(retained, expectedRetained, released, expectedReleased);
-    Assertions.assertEquals(expectedRetained == expectedReleased, returned);
+    Assertions.assertEquals(expectedRetained == expectedReleased, 
completelyReleased);
   }
 
-  @Test
-  @Timeout(value = 1)
-  public void testWrap() {
-    final String value = "testWrap";
-    final AtomicInteger retained = new AtomicInteger();
-    final AtomicInteger released = new AtomicInteger();
-    final ReferenceCountedObject<String> ref = ReferenceCountedObject.wrap(
-        value, retained::getAndIncrement, released::getAndIncrement);
-
+  static void runTestWrapper(String value, ReferenceCountedObject<String> ref,
+      AtomicInteger retained, AtomicInteger released) {
     assertValues(retained, 0, released, 0);
     try {
       ref.get();
@@ -94,6 +89,24 @@ public class TestReferenceCountedObject {
       e.printStackTrace(System.out);
     }
 
+    assertValues(retained, 4, released, 4);
+  }
+
+  @Test
+  @Timeout(value = 1)
+  public void testValueWrapper() {
+    final String value = "testValue";
+    final AtomicInteger retained = new AtomicInteger();
+    final AtomicInteger released = new AtomicInteger();
+    final ReferenceCountedObject<String> ref = 
ReferenceCountedObject.<String>newBuilder()
+        .setValue(value)
+        .setRetainMethod(retained::getAndIncrement)
+        .setReleaseMethod(released::getAndIncrement)
+        .build();
+
+    runTestWrapper(value, ref, retained, released);
+
+    // for the ValueWrapper, it cannot be retained/released after it is 
completely released
     try {
       ref.retain();
       Assertions.fail();
@@ -115,10 +128,46 @@ public class TestReferenceCountedObject {
     }
   }
 
+  @Test
+  @Timeout(value = 1)
+  public void testConstructorWrapper() {
+    final String prefix = "constructor";
+    final AtomicInteger valueCount = new AtomicInteger();
+    final AtomicInteger retained = new AtomicInteger();
+    final AtomicInteger released = new AtomicInteger();
+    final ReferenceCountedObject<String> ref = 
ReferenceCountedObject.<String>newBuilder()
+        .setConstructor(() -> prefix + valueCount.getAndIncrement())
+        .setRetainMethod(retained::getAndIncrement)
+        .setReleaseMethod(released::getAndIncrement)
+        .build();
+    Assertions.assertEquals(0, valueCount.get());
+    runTestWrapper(prefix + valueCount, ref, retained, released);
+    Assertions.assertEquals(1, valueCount.get());
+
+    // for the ConstructorWrapper, it can be retained/released after it is 
completely released
+    retained.set(0);
+    released.set(0);
+    runTestWrapper(prefix + valueCount, ref, retained, released);
+    Assertions.assertEquals(2, valueCount.get());
+  }
+
+  @Test
+  @Timeout(value = 1)
+  public void testBuilder() {
+    // Do not set value and constructor
+    assertThrows(NullPointerException.class, () -> 
ReferenceCountedObject.newBuilder().build());
+
+    // Set both value and constructor
+    assertThrows(IllegalStateException.class, () -> 
ReferenceCountedObject.newBuilder()
+        .setValue("")
+        .setConstructor(() -> "")
+        .build());
+  }
+
   @Test
   @Timeout(value = 1)
   public void testReleaseWithoutRetaining() {
-    final ReferenceCountedObject<String> ref = ReferenceCountedObject.wrap("");
+    final ReferenceCountedObject<Object> ref = 
ReferenceCountedObject.newBuilder().setValue("").build();
 
     try {
       ref.release();

Reply via email to