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

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new 5ddbe8c2d5 feat: add logging variants for Java DSL stash and timer 
base classes (#3387)
5ddbe8c2d5 is described below

commit 5ddbe8c2d550b8e1c7dab00005e9fc80c15d0a02
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sun Jul 26 23:53:35 2026 +0800

    feat: add logging variants for Java DSL stash and timer base classes (#3387)
    
    Motivation:
    Java does not support multiple inheritance or trait mixin, so users
    cannot combine AbstractActorWithStash/AbstractActorWithTimers with
    ActorLogging. This forces Java users to manually implement logging
    boilerplate when they need both stash/timer support and logging.
    
    Modification:
    Add 8 new abstract base classes that combine ActorLogging with each
    stash and timer variant:
    - AbstractLoggingActorWithStash / UntypedAbstractLoggingActorWithStash
    - AbstractLoggingActorWithUnboundedStash / Untyped variant
    - AbstractLoggingActorWithUnrestrictedStash / Untyped variant
    - AbstractLoggingActorWithTimers / UntypedAbstractLoggingActorWithTimers
    
    Result:
    Java users can now extend a single base class to get both stash/timer
    support and logging, matching the convenience Scala users get via
    trait mixin.
    
    Tests:
    - sbt "actor-tests / Test / testOnly 
org.apache.pekko.actor.LoggingVariantActorsTest"
      Passed: Total 9, Failed 0, Errors 0, Passed 9
    - sbt "actor / mimaReportBinaryIssues" - success (pure additions)
    
    References:
    Fixes #2520
---
 .../pekko/actor/LoggingVariantActorsTest.java      | 512 +++++++++++++++++++++
 .../org/apache/pekko/actor/AbstractActor.scala     |  75 +++
 .../main/scala/org/apache/pekko/actor/Timers.scala |  32 ++
 3 files changed, 619 insertions(+)

diff --git 
a/actor-tests/src/test/java/org/apache/pekko/actor/LoggingVariantActorsTest.java
 
b/actor-tests/src/test/java/org/apache/pekko/actor/LoggingVariantActorsTest.java
new file mode 100644
index 0000000000..ca73c7e56d
--- /dev/null
+++ 
b/actor-tests/src/test/java/org/apache/pekko/actor/LoggingVariantActorsTest.java
@@ -0,0 +1,512 @@
+/*
+ * 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.pekko.actor;
+
+import java.time.Duration;
+
+import scala.concurrent.duration.FiniteDuration;
+
+import com.typesafe.config.ConfigFactory;
+
+import org.apache.pekko.event.Logging;
+import org.apache.pekko.testkit.PekkoJUnitJupiterActorSystemResource;
+import org.apache.pekko.testkit.TestProbe;
+import org.apache.pekko.testkit.javadsl.EventFilter;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LoggingVariantActorsTest {
+
+  @RegisterExtension
+  static PekkoJUnitJupiterActorSystemResource actorSystemResource =
+      new PekkoJUnitJupiterActorSystemResource(
+          "LoggingVariantActorsTest",
+          ConfigFactory.parseString("pekko.loggers = 
[org.apache.pekko.testkit.TestEventListener]")
+              .withFallback(ActorWithBoundedStashSpec.testConf()));
+
+  private final ActorSystem system = actorSystemResource.getSystem();
+
+  // --- stash actor implementations ---
+
+  public static class WithStashAndLogging extends 
AbstractLoggingActorWithStash {
+    int count = 0;
+
+    @Override
+    public Receive createReceive() {
+      return receiveBuilder()
+          .matchEquals(
+              "fail",
+              s -> {
+                throw new RuntimeException("restart requested");
+              })
+          .match(
+              String.class,
+              s -> {
+                if (count < 0) {
+                  log().info("Replying with length of: {}", s);
+                  getSender().tell(s.length(), getSelf());
+                } else if (count == 2) {
+                  unstashAll();
+                  count = -1;
+                } else {
+                  stash();
+                  count += 1;
+                }
+              })
+          .build();
+    }
+  }
+
+  public static class UntypedWithStashAndLogging extends 
UntypedAbstractLoggingActorWithStash {
+    int count = 0;
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+      if ("fail".equals(msg)) {
+        throw new RuntimeException("restart requested");
+      } else if (msg instanceof String s) {
+        if (count < 0) {
+          log().info("Replying with length of: {}", s);
+          getSender().tell(s.length(), getSelf());
+        } else if (count == 2) {
+          unstashAll();
+          count = -1;
+        } else {
+          stash();
+          count += 1;
+        }
+      } else {
+        unhandled(msg);
+      }
+    }
+  }
+
+  public static class WithUnboundedStashAndLogging extends 
AbstractLoggingActorWithUnboundedStash {
+    int count = 0;
+
+    @Override
+    public Receive createReceive() {
+      return receiveBuilder()
+          .match(
+              String.class,
+              s -> {
+                if (count < 0) {
+                  log().info("Replying with length of: {}", s);
+                  getSender().tell(s.length(), getSelf());
+                } else if (count == 2) {
+                  unstashAll();
+                  count = -1;
+                } else {
+                  stash();
+                  count += 1;
+                }
+              })
+          .build();
+    }
+  }
+
+  public static class UntypedWithUnboundedStashAndLogging
+      extends UntypedAbstractLoggingActorWithUnboundedStash {
+    int count = 0;
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+      if (msg instanceof String s) {
+        if (count < 0) {
+          log().info("Replying with length of: {}", s);
+          getSender().tell(s.length(), getSelf());
+        } else if (count == 2) {
+          unstashAll();
+          count = -1;
+        } else {
+          stash();
+          count += 1;
+        }
+      } else {
+        unhandled(msg);
+      }
+    }
+  }
+
+  public static class WithUnrestrictedStashAndLogging
+      extends AbstractLoggingActorWithUnrestrictedStash {
+    int count = 0;
+
+    @Override
+    public Receive createReceive() {
+      return receiveBuilder()
+          .match(
+              String.class,
+              s -> {
+                if (count < 0) {
+                  log().info("Replying with length of: {}", s);
+                  getSender().tell(s.length(), getSelf());
+                } else if (count == 2) {
+                  unstashAll();
+                  count = -1;
+                } else {
+                  stash();
+                  count += 1;
+                }
+              })
+          .build();
+    }
+  }
+
+  public static class UntypedWithUnrestrictedStashAndLogging
+      extends UntypedAbstractLoggingActorWithUnrestrictedStash {
+    int count = 0;
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+      if (msg instanceof String s) {
+        if (count < 0) {
+          log().info("Replying with length of: {}", s);
+          getSender().tell(s.length(), getSelf());
+        } else if (count == 2) {
+          unstashAll();
+          count = -1;
+        } else {
+          stash();
+          count += 1;
+        }
+      } else {
+        unhandled(msg);
+      }
+    }
+  }
+
+  // --- timer actor implementations ---
+
+  public static class WithTimersAndLogging extends 
AbstractLoggingActorWithTimers {
+    private ActorRef replyTo;
+
+    @Override
+    public Receive createReceive() {
+      return receiveBuilder()
+          .matchEquals(
+              "tick",
+              s -> {
+                log().info("Timer fired");
+                replyTo.tell("done", getSelf());
+              })
+          .matchEquals(
+              "cancel",
+              s -> {
+                log().info("Cancelling timer");
+                getTimers().cancel("key");
+                replyTo.tell("cancelled", getSelf());
+              })
+          .match(
+              String.class,
+              s -> {
+                log().info("Received: {}", s);
+                replyTo = getSender();
+                getTimers().startSingleTimer("key", "tick", 
Duration.ofMillis(200));
+              })
+          .build();
+    }
+  }
+
+  public static class UntypedWithTimersAndLogging extends 
UntypedAbstractLoggingActorWithTimers {
+    private ActorRef replyTo;
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+      if ("tick".equals(msg)) {
+        log().info("Timer fired");
+        replyTo.tell("done", getSelf());
+      } else if ("cancel".equals(msg)) {
+        log().info("Cancelling timer");
+        getTimers().cancel("key");
+        replyTo.tell("cancelled", getSelf());
+      } else if (msg instanceof String s) {
+        log().info("Received: {}", s);
+        replyTo = getSender();
+        getTimers().startSingleTimer("key", "tick", Duration.ofMillis(200));
+      } else {
+        unhandled(msg);
+      }
+    }
+  }
+
+  // --- stash functional tests ---
+
+  private void testStashFunctionality(Props props) {
+    ActorRef ref = system.actorOf(props);
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "Hello");
+    probe.send(ref, "Hello2");
+    probe.send(ref, "Hello12");
+    probe.expectMsg(5);
+    probe.expectMsg(6);
+  }
+
+  @Test
+  public void mustSupportStashWithLogging() {
+    testStashFunctionality(Props.create(WithStashAndLogging.class));
+  }
+
+  @Test
+  public void mustSupportUntypedStashWithLogging() {
+    testStashFunctionality(Props.create(UntypedWithStashAndLogging.class));
+  }
+
+  @Test
+  public void mustSupportUnboundedStashWithLogging() {
+    testStashFunctionality(Props.create(WithUnboundedStashAndLogging.class));
+  }
+
+  @Test
+  public void mustSupportUntypedUnboundedStashWithLogging() {
+    
testStashFunctionality(Props.create(UntypedWithUnboundedStashAndLogging.class));
+  }
+
+  @Test
+  public void mustSupportUnrestrictedStashWithLogging() {
+    testStashFunctionality(
+        Props.create(WithUnrestrictedStashAndLogging.class)
+            .withMailbox("pekko.actor.mailbox.unbounded-deque-based"));
+  }
+
+  @Test
+  public void mustSupportUntypedUnrestrictedStashWithLogging() {
+    testStashFunctionality(
+        Props.create(UntypedWithUnrestrictedStashAndLogging.class)
+            .withMailbox("pekko.actor.mailbox.unbounded-deque-based"));
+  }
+
+  // --- timer functional tests ---
+
+  @Test
+  public void mustSupportTimersWithLogging() {
+    ActorRef ref = system.actorOf(Props.create(WithTimersAndLogging.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "start");
+    probe.expectMsg(FiniteDuration.create(3, "seconds"), "done");
+  }
+
+  @Test
+  public void mustSupportUntypedTimersWithLogging() {
+    ActorRef ref = 
system.actorOf(Props.create(UntypedWithTimersAndLogging.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "start");
+    probe.expectMsg(FiniteDuration.create(3, "seconds"), "done");
+  }
+
+  @Test
+  public void mustCancelTimerWithLogging() {
+    ActorRef ref = system.actorOf(Props.create(WithTimersAndLogging.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "start");
+    probe.send(ref, "cancel");
+    probe.expectMsg(FiniteDuration.create(3, "seconds"), "cancelled");
+    probe.expectNoMessage(FiniteDuration.create(500, "millis"));
+  }
+
+  @Test
+  public void mustCancelUntypedTimerWithLogging() {
+    ActorRef ref = 
system.actorOf(Props.create(UntypedWithTimersAndLogging.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "start");
+    probe.send(ref, "cancel");
+    probe.expectMsg(FiniteDuration.create(3, "seconds"), "cancelled");
+    probe.expectNoMessage(FiniteDuration.create(500, "millis"));
+  }
+
+  // --- logging event assertions for all variants ---
+
+  @Test
+  public void mustEmitLogEventsFromStashVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Replying with length of")
+        .occurrences(2)
+        .intercept(
+            () -> {
+              testStashFunctionality(Props.create(WithStashAndLogging.class));
+              return null;
+            });
+  }
+
+  @Test
+  public void mustEmitLogEventsFromUntypedStashVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Replying with length of")
+        .occurrences(2)
+        .intercept(
+            () -> {
+              
testStashFunctionality(Props.create(UntypedWithStashAndLogging.class));
+              return null;
+            });
+  }
+
+  @Test
+  public void mustEmitLogEventsFromUnboundedStashVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Replying with length of")
+        .occurrences(2)
+        .intercept(
+            () -> {
+              
testStashFunctionality(Props.create(WithUnboundedStashAndLogging.class));
+              return null;
+            });
+  }
+
+  @Test
+  public void mustEmitLogEventsFromUntypedUnboundedStashVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Replying with length of")
+        .occurrences(2)
+        .intercept(
+            () -> {
+              
testStashFunctionality(Props.create(UntypedWithUnboundedStashAndLogging.class));
+              return null;
+            });
+  }
+
+  @Test
+  public void mustEmitLogEventsFromUnrestrictedStashVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Replying with length of")
+        .occurrences(2)
+        .intercept(
+            () -> {
+              testStashFunctionality(
+                  Props.create(WithUnrestrictedStashAndLogging.class)
+                      
.withMailbox("pekko.actor.mailbox.unbounded-deque-based"));
+              return null;
+            });
+  }
+
+  @Test
+  public void mustEmitLogEventsFromUntypedUnrestrictedStashVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Replying with length of")
+        .occurrences(2)
+        .intercept(
+            () -> {
+              testStashFunctionality(
+                  Props.create(UntypedWithUnrestrictedStashAndLogging.class)
+                      
.withMailbox("pekko.actor.mailbox.unbounded-deque-based"));
+              return null;
+            });
+  }
+
+  @Test
+  public void mustEmitLogEventsFromTimerVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Timer fired")
+        .occurrences(1)
+        .intercept(
+            () -> {
+              ActorRef ref = 
system.actorOf(Props.create(WithTimersAndLogging.class));
+              final TestProbe probe = new TestProbe(system);
+              probe.send(ref, "start");
+              probe.expectMsg(FiniteDuration.create(3, "seconds"), "done");
+              return null;
+            });
+  }
+
+  @Test
+  public void mustEmitLogEventsFromUntypedTimerVariant() {
+    new EventFilter(Logging.Info.class, system)
+        .startsWith("Timer fired")
+        .occurrences(1)
+        .intercept(
+            () -> {
+              ActorRef ref = 
system.actorOf(Props.create(UntypedWithTimersAndLogging.class));
+              final TestProbe probe = new TestProbe(system);
+              probe.send(ref, "start");
+              probe.expectMsg(FiniteDuration.create(3, "seconds"), "done");
+              return null;
+            });
+  }
+
+  // --- stash restart behavior (unstash on preRestart) ---
+
+  @Test
+  public void mustUnstashOnRestartForStashVariant() {
+    ActorRef ref = system.actorOf(Props.create(WithStashAndLogging.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "Hello");
+    probe.send(ref, "fail");
+    probe.send(ref, "Hello2");
+    probe.send(ref, "Hello12");
+    probe.expectMsg(FiniteDuration.create(10, "seconds"), 5);
+  }
+
+  @Test
+  public void mustUnstashOnRestartForUntypedStashVariant() {
+    ActorRef ref = 
system.actorOf(Props.create(UntypedWithStashAndLogging.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "Hello");
+    probe.send(ref, "fail");
+    probe.send(ref, "Hello2");
+    probe.send(ref, "Hello12");
+    probe.expectMsg(FiniteDuration.create(10, "seconds"), 5);
+  }
+
+  // --- log adapter accessibility ---
+
+  public static class LogAccessorCheck extends AbstractLoggingActorWithStash {
+    @Override
+    public Receive createReceive() {
+      return receiveBuilder()
+          .matchEquals(
+              "check",
+              s -> {
+                assertNotNull(log());
+                assertTrue(log().isInfoEnabled());
+                getSender().tell("ok", getSelf());
+              })
+          .build();
+    }
+  }
+
+  public static class UntypedLogAccessorCheck extends 
UntypedAbstractLoggingActorWithTimers {
+    @Override
+    public void onReceive(Object msg) throws Exception {
+      if ("check".equals(msg)) {
+        assertNotNull(log());
+        assertNotNull(getTimers());
+        assertTrue(log().isInfoEnabled());
+        getSender().tell("ok", getSelf());
+      } else {
+        unhandled(msg);
+      }
+    }
+  }
+
+  @Test
+  public void mustProvideLogAccessor() {
+    ActorRef ref = system.actorOf(Props.create(LogAccessorCheck.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "check");
+    probe.expectMsg(FiniteDuration.create(3, "seconds"), "ok");
+  }
+
+  @Test
+  public void mustProvideLogAndTimersAccessors() {
+    ActorRef ref = system.actorOf(Props.create(UntypedLogAccessorCheck.class));
+    final TestProbe probe = new TestProbe(system);
+    probe.send(ref, "check");
+    probe.expectMsg(FiniteDuration.create(3, "seconds"), "ok");
+  }
+}
diff --git a/actor/src/main/scala/org/apache/pekko/actor/AbstractActor.scala 
b/actor/src/main/scala/org/apache/pekko/actor/AbstractActor.scala
index 7bb3ce71a0..d8ed444a01 100644
--- a/actor/src/main/scala/org/apache/pekko/actor/AbstractActor.scala
+++ b/actor/src/main/scala/org/apache/pekko/actor/AbstractActor.scala
@@ -492,3 +492,78 @@ abstract class AbstractActorWithUnrestrictedStash extends 
AbstractActor with Abs
  * @since 2.0.0
  */
 abstract class UntypedAbstractActorWithUnrestrictedStash extends 
UntypedAbstractActor with AbstractActorStashSupport
+
+/**
+ * Java API: compatible with lambda expressions
+ *
+ * Actor base class that mixes in logging and a stash.
+ * By default requests a `DequeBasedMessageQueueSemantics` mailbox.
+ *
+ * @see [[pekko.actor.AbstractActorWithStash]] for details on how `Stash` 
works.
+ *
+ * @since 2.0.0
+ */
+abstract class AbstractLoggingActorWithStash extends AbstractActor with 
AbstractActorStashSupport
+    with RequiresMessageQueue[DequeBasedMessageQueueSemantics] with 
ActorLogging
+
+/**
+ * Java API: compatible with lambda expressions
+ *
+ * Actor base class that mixes in logging and a stash.
+ * By default requests a `DequeBasedMessageQueueSemantics` mailbox.
+ *
+ * @see [[pekko.actor.AbstractActorWithStash]] for details on how `Stash` 
works.
+ *
+ * @since 2.0.0
+ */
+abstract class UntypedAbstractLoggingActorWithStash extends 
UntypedAbstractActor with AbstractActorStashSupport
+    with RequiresMessageQueue[DequeBasedMessageQueueSemantics] with 
ActorLogging
+
+/**
+ * Java API: compatible with lambda expressions
+ *
+ * Actor base class that mixes in logging and an unbounded stash.
+ *
+ * @see [[pekko.actor.AbstractActorWithUnboundedStash]] for details on how 
`Stash` works.
+ *
+ * @since 2.0.0
+ */
+abstract class AbstractLoggingActorWithUnboundedStash extends AbstractActor 
with AbstractActorStashSupport
+    with RequiresMessageQueue[UnboundedDequeBasedMessageQueueSemantics] with 
ActorLogging
+
+/**
+ * Java API: compatible with lambda expressions
+ *
+ * Actor base class that mixes in logging and an unbounded stash.
+ *
+ * @see [[pekko.actor.AbstractActorWithUnboundedStash]] for details on how 
`Stash` works.
+ *
+ * @since 2.0.0
+ */
+abstract class UntypedAbstractLoggingActorWithUnboundedStash extends 
UntypedAbstractActor
+    with AbstractActorStashSupport
+    with RequiresMessageQueue[UnboundedDequeBasedMessageQueueSemantics] with 
ActorLogging
+
+/**
+ * Java API: compatible with lambda expressions
+ *
+ * Actor base class that mixes in logging and an unrestricted stash.
+ *
+ * @see [[pekko.actor.AbstractActorWithUnrestrictedStash]] for details on how 
`Stash` works.
+ *
+ * @since 2.0.0
+ */
+abstract class AbstractLoggingActorWithUnrestrictedStash extends AbstractActor 
with AbstractActorStashSupport
+    with ActorLogging
+
+/**
+ * Java API: compatible with lambda expressions
+ *
+ * Actor base class that mixes in logging and an unrestricted stash.
+ *
+ * @see [[pekko.actor.AbstractActorWithUnrestrictedStash]] for details on how 
`Stash` works.
+ *
+ * @since 2.0.0
+ */
+abstract class UntypedAbstractLoggingActorWithUnrestrictedStash
+    extends UntypedAbstractActor with AbstractActorStashSupport with 
ActorLogging
diff --git a/actor/src/main/scala/org/apache/pekko/actor/Timers.scala 
b/actor/src/main/scala/org/apache/pekko/actor/Timers.scala
index c75574be4d..7cbc1bcebc 100644
--- a/actor/src/main/scala/org/apache/pekko/actor/Timers.scala
+++ b/actor/src/main/scala/org/apache/pekko/actor/Timers.scala
@@ -98,6 +98,38 @@ abstract class UntypedAbstractActorWithTimers extends 
UntypedAbstractActor with
   final def getTimers: TimerScheduler = timers
 }
 
+/**
+ * Java API: Support for scheduled `self` messages via [[TimerScheduler]], 
with logging.
+ *
+ * Timers are bound to the lifecycle of the actor that owns it,
+ * and thus are cancelled automatically when it is restarted or stopped.
+ *
+ * @since 2.0.0
+ */
+abstract class AbstractLoggingActorWithTimers extends AbstractActor with 
Timers with ActorLogging {
+
+  /**
+   * Start and cancel timers via the enclosed `TimerScheduler`.
+   */
+  final def getTimers: TimerScheduler = timers
+}
+
+/**
+ * Java API: Support for scheduled `self` messages via [[TimerScheduler]], 
with logging.
+ *
+ * Timers are bound to the lifecycle of the actor that owns it,
+ * and thus are cancelled automatically when it is restarted or stopped.
+ *
+ * @since 2.0.0
+ */
+abstract class UntypedAbstractLoggingActorWithTimers extends 
UntypedAbstractActor with Timers with ActorLogging {
+
+  /**
+   * Start and cancel timers via the enclosed `TimerScheduler`.
+   */
+  final def getTimers: TimerScheduler = timers
+}
+
 /**
  * Support for scheduled `self` messages in an actor.
  * It is used by mixing in trait `Timers` in Scala or extending 
`AbstractActorWithTimers`


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to