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

pjfanning 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 90b02d6068 fix: make AbstractPersistentActorWithTimers and 
AbstractFSMWithStash subclassable from Java on Scala 3 (#3475)
90b02d6068 is described below

commit 90b02d6068164d0a3a0593cb6d2dc99a08edbd0d
Author: PJ Fanning <[email protected]>
AuthorDate: Thu Aug 27 13:00:09 2026 +0100

    fix: make AbstractPersistentActorWithTimers and AbstractFSMWithStash 
subclassable from Java on Scala 3 (#3475)
    
    * fix: make AbstractPersistentActorWithTimers and AbstractFSMWithStash 
subclassable from Java on Scala 3
    
    Motivation:
    Extending `AbstractPersistentActorWithTimers` from Java fails to compile 
against
    the Scala 3 artifacts:
    
        class TestActor inherits unrelated defaults for
        aroundPreRestart(Throwable,Option<Object>) from types Timers and 
Eventsourced
    
    Both `Timers` and `Eventsourced` implement `aroundReceive`, 
`aroundPreRestart`
    and `aroundPostStop`. Scala 3 does emit the mixin forwarder into the class, 
but
    flags it `ACC_BRIDGE, ACC_SYNTHETIC`; javac ignores synthetic and bridge 
members
    when resolving inherited members, so it falls back to the two interface 
defaults
    and rejects the subclass. Scala 2.13 emits the same forwarder without those
    flags, which is why the 2.12/2.13 artifacts work.
    
    A javac probe over every Java-facing `Abstract*` class that mixes in more 
than
    one trait found one other class with the same defect: 
`AbstractFSMWithStash`,
    where `FSM` and `UnrestrictedStash` both implement `postStop`.
    
    Modification:
    Give both classes real (non-synthetic) overrides of the conflicting members 
that
    just delegate to `super`, which resolves exactly like the forwarders they
    replace. `AbstractFSMWithStash.postStop` deliberately carries no
    `@throws(classOf[Exception])`, since `FSM.postStop` declares no checked
    exceptions and a wider throws clause is not a valid override for javac.
    
    Add Java sources exercising both classes so the Scala 3 build fails if this
    regresses.
    
    Result:
    Java subclasses of `AbstractPersistentActorWithTimers` and
    `AbstractFSMWithStash` compile against the Scala 3 artifacts. No other
    Java-facing multi-trait base class in the build is affected.
    
    Tests:
    - sbt "++3.3.8" "persistence/testOnly 
org.apache.pekko.persistence.TimerPersistentActorSpec" - 5 succeeded, 0 failed
    - sbt "++3.3.8" "actor-tests/testOnly 
org.apache.pekko.actor.AbstractFSMWithStashActorTest 
org.apache.pekko.actor.AbstractFSMActorTest" - 2 succeeded, 0 failed
    - sbt "persistence/testOnly 
org.apache.pekko.persistence.TimerPersistentActorSpec" - 5 succeeded, 0 failed
    - sbt "actor-tests/testOnly 
org.apache.pekko.actor.AbstractFSMWithStashActorTest 
org.apache.pekko.actor.AbstractFSMActorTest" - 2 succeeded, 0 failed
    - sbt headerCreateAll javafmtAll scalafmtAll scalafmtSbt (JDK 17) - no 
churn outside the changed files
    - sbt +mimaReportBinaryIssues - Not run, left to CI Binary Compatibility job
    
    References:
    Fixes #3474
    
    * compile checks
    
    * fix: mark deprecated PersistentFSM compile guard as deprecated for -Werror
---
 .../pekko/actor/AbstractFSMWithStashActorTest.java | 83 ++++++++++++++++++++++
 .../pekko/actor/JavaSubclassCompilationCheck.java  | 46 ++++++++++++
 .../scala/org/apache/pekko/actor/AbstractFSM.scala | 13 +++-
 .../apache/pekko/persistence/PersistentActor.scala | 18 ++++-
 .../persistence/JavaTimerPersistentActor.java      | 69 ++++++++++++++++++
 .../fsm/JavaSubclassCompilationCheck.java          | 37 ++++++++++
 .../persistence/TimerPersistentActorSpec.scala     |  6 ++
 .../stream/stage/JavaSubclassCompilationCheck.java | 32 +++++++++
 8 files changed, 302 insertions(+), 2 deletions(-)

diff --git 
a/actor-tests/src/test/java/org/apache/pekko/actor/AbstractFSMWithStashActorTest.java
 
b/actor-tests/src/test/java/org/apache/pekko/actor/AbstractFSMWithStashActorTest.java
new file mode 100644
index 0000000000..edcfcc854b
--- /dev/null
+++ 
b/actor-tests/src/test/java/org/apache/pekko/actor/AbstractFSMWithStashActorTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 org.apache.pekko.testkit.PekkoJUnitJupiterActorSystemResource;
+import org.apache.pekko.testkit.PekkoSpec;
+import org.apache.pekko.testkit.TestProbe;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/**
+ * Subclassing {@link AbstractFSMWithStash} from Java has to keep compiling: 
both {@code FSM} and
+ * {@code UnrestrictedStash} implement {@code postStop}, and javac rejects a 
subclass unless the
+ * Scala base class carries a real (non-synthetic) override for it.
+ */
+@SuppressWarnings("unchecked")
+public class AbstractFSMWithStashActorTest {
+
+  public static class MyFSM extends AbstractFSMWithStash<String, String> {
+
+    private final ActorRef probe;
+
+    MyFSM(ActorRef probe) {
+      this.probe = probe;
+      startWith("start", "data");
+      when(
+          "start",
+          matchEvent(
+              String.class,
+              (event, data) -> {
+                if ("go".equals(event)) {
+                  unstashAll();
+                  return goTo("next");
+                } else {
+                  stash();
+                  return stay();
+                }
+              }));
+      when(
+          "next",
+          matchEvent(
+              String.class,
+              (event, data) -> {
+                probe.tell(event, getSelf());
+                return stay();
+              }));
+      initialize();
+    }
+  }
+
+  @RegisterExtension
+  static PekkoJUnitJupiterActorSystemResource actorSystemResource =
+      new PekkoJUnitJupiterActorSystemResource(
+          "AbstractFSMWithStashActorTest", PekkoSpec.testConf());
+
+  private final ActorSystem system = actorSystemResource.getSystem();
+
+  @Test
+  public void canCreateFSMWithStash() {
+    TestProbe probe = new TestProbe(system);
+
+    ActorRef ref = system.actorOf(Props.create(MyFSM.class, probe.ref()));
+    ref.tell("work", ActorRef.noSender());
+    ref.tell("go", ActorRef.noSender());
+
+    probe.expectMsg("work");
+  }
+}
diff --git 
a/actor-tests/src/test/java/org/apache/pekko/actor/JavaSubclassCompilationCheck.java
 
b/actor-tests/src/test/java/org/apache/pekko/actor/JavaSubclassCompilationCheck.java
new file mode 100644
index 0000000000..183f895a2d
--- /dev/null
+++ 
b/actor-tests/src/test/java/org/apache/pekko/actor/JavaSubclassCompilationCheck.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+/**
+ * Compile-only guard for Java-facing base classes that mix in more than one 
Scala trait.
+ *
+ * <p>Scala 3 emits the mixin forwarder for a member implemented by two 
mixed-in traits as an
+ * ACC_BRIDGE/ACC_SYNTHETIC method. javac ignores synthetic and bridge members 
when resolving
+ * inherited members, so it falls back to the interface defaults and rejects 
the subclass with
+ * "inherits unrelated defaults" (see #3474). Merely declaring a Java subclass 
is enough to catch
+ * that, since it is a compile error.
+ *
+ * <p>The classes below have no other Java subclass in the build under JDK 17, 
so without this file
+ * they carry no Java compilation coverage at all.
+ */
+public final class JavaSubclassCompilationCheck {
+
+  private JavaSubclassCompilationCheck() {}
+
+  abstract static class LoggingActor extends UntypedAbstractLoggingActor {}
+
+  // The `UntypedAbstractActor*Stash` classes are otherwise only subclassed 
under
+  // `src/test/java-jdk21-only`, which is compiled by the separate `TestJdk21` 
configuration.
+  abstract static class ActorWithStash extends UntypedAbstractActorWithStash {}
+
+  abstract static class ActorWithUnboundedStash extends 
UntypedAbstractActorWithUnboundedStash {}
+
+  abstract static class ActorWithUnrestrictedStash
+      extends UntypedAbstractActorWithUnrestrictedStash {}
+}
diff --git a/actor/src/main/scala/org/apache/pekko/actor/AbstractFSM.scala 
b/actor/src/main/scala/org/apache/pekko/actor/AbstractFSM.scala
index e8e33c15e8..7c631b2686 100644
--- a/actor/src/main/scala/org/apache/pekko/actor/AbstractFSM.scala
+++ b/actor/src/main/scala/org/apache/pekko/actor/AbstractFSM.scala
@@ -533,4 +533,15 @@ abstract class AbstractLoggingFSM[S, D] extends 
AbstractFSM[S, D] with LoggingFS
  *
  * Finite State Machine actor abstract base class with Stash support.
  */
-abstract class AbstractFSMWithStash[S, D] extends AbstractFSM[S, D] with Stash
+abstract class AbstractFSMWithStash[S, D] extends AbstractFSM[S, D] with Stash 
{
+
+  // Overridden solely so that this class carries a real (non-synthetic) 
override of the member that
+  // both `FSM` and `UnrestrictedStash` implement. Scala 3 emits the mixin 
forwarder as an
+  // ACC_BRIDGE/ACC_SYNTHETIC method, which javac ignores when resolving 
inherited members, so a Java
+  // subclass would otherwise fail to compile with "inherits unrelated 
defaults". The body just
+  // delegates to `super`, which resolves exactly like the forwarder it 
replaces.
+
+  // No `@throws(classOf[Exception])` here: `FSM.postStop` declares no checked 
exceptions, so a wider
+  // throws clause would not be a valid override for javac.
+  override def postStop(): Unit = super.postStop()
+}
diff --git 
a/persistence/src/main/scala/org/apache/pekko/persistence/PersistentActor.scala 
b/persistence/src/main/scala/org/apache/pekko/persistence/PersistentActor.scala
index 17ada6f8c7..21f85893ea 100644
--- 
a/persistence/src/main/scala/org/apache/pekko/persistence/PersistentActor.scala
+++ 
b/persistence/src/main/scala/org/apache/pekko/persistence/PersistentActor.scala
@@ -488,4 +488,20 @@ abstract class AbstractPersistentActor extends 
AbstractActor with AbstractPersis
 /**
  * Java API: Combination of [[AbstractPersistentActor]] and 
[[pekko.actor.AbstractActorWithTimers]].
  */
-abstract class AbstractPersistentActorWithTimers extends AbstractActor with 
Timers with AbstractPersistentActorLike
+abstract class AbstractPersistentActorWithTimers extends AbstractActor with 
Timers with AbstractPersistentActorLike {
+
+  // The methods below are overridden solely so that this class carries real 
(non-synthetic) overrides
+  // for the members that both `Timers` and `Eventsourced` implement. Scala 3 
emits the mixin forwarders
+  // as ACC_BRIDGE/ACC_SYNTHETIC methods, which javac ignores when resolving 
inherited members, so a Java
+  // subclass would otherwise fail to compile with "inherits unrelated 
defaults". The bodies just delegate
+  // to `super`, which resolves exactly like the forwarders they replace.
+
+  override protected[pekko] def aroundReceive(receive: Actor.Receive, msg: 
Any): Unit =
+    super.aroundReceive(receive, msg)
+
+  override protected[pekko] def aroundPreRestart(reason: Throwable, message: 
Option[Any]): Unit =
+    super.aroundPreRestart(reason, message)
+
+  override protected[pekko] def aroundPostStop(): Unit =
+    super.aroundPostStop()
+}
diff --git 
a/persistence/src/test/java/org/apache/pekko/persistence/JavaTimerPersistentActor.java
 
b/persistence/src/test/java/org/apache/pekko/persistence/JavaTimerPersistentActor.java
new file mode 100644
index 0000000000..6dfc502cbe
--- /dev/null
+++ 
b/persistence/src/test/java/org/apache/pekko/persistence/JavaTimerPersistentActor.java
@@ -0,0 +1,69 @@
+/*
+ * 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.persistence;
+
+import java.time.Duration;
+import org.apache.pekko.actor.ActorRef;
+
+/**
+ * Subclassing {@link AbstractPersistentActorWithTimers} from Java has to keep 
compiling: both
+ * {@code Timers} and {@code Eventsourced} implement the {@code 
aroundReceive}/{@code
+ * aroundPreRestart}/ {@code aroundPostStop} members, and javac rejects a 
subclass unless the Scala
+ * base class carries real (non-synthetic) overrides for them.
+ */
+@SuppressWarnings("unchecked")
+public class JavaTimerPersistentActor extends 
AbstractPersistentActorWithTimers {
+
+  public static final class Scheduled {
+    public final Object msg;
+    public final ActorRef replyTo;
+
+    public Scheduled(Object msg, ActorRef replyTo) {
+      this.msg = msg;
+      this.replyTo = replyTo;
+    }
+  }
+
+  private final String name;
+
+  public JavaTimerPersistentActor(String name) {
+    this.name = name;
+  }
+
+  @Override
+  public String persistenceId() {
+    return name;
+  }
+
+  @Override
+  public Receive createReceiveRecover() {
+    return receiveBuilder().matchAny(msg -> {}).build();
+  }
+
+  @Override
+  public Receive createReceive() {
+    return receiveBuilder()
+        .match(Scheduled.class, scheduled -> 
scheduled.replyTo.tell(scheduled.msg, getSelf()))
+        .matchAny(
+            msg -> {
+              timers().startSingleTimer("key", new Scheduled(msg, 
getSender()), Duration.ZERO);
+              persist(msg, evt -> {});
+            })
+        .build();
+  }
+}
diff --git 
a/persistence/src/test/java/org/apache/pekko/persistence/fsm/JavaSubclassCompilationCheck.java
 
b/persistence/src/test/java/org/apache/pekko/persistence/fsm/JavaSubclassCompilationCheck.java
new file mode 100644
index 0000000000..72b6628355
--- /dev/null
+++ 
b/persistence/src/test/java/org/apache/pekko/persistence/fsm/JavaSubclassCompilationCheck.java
@@ -0,0 +1,37 @@
+/*
+ * 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.persistence.fsm;
+
+/**
+ * Compile-only guard for Java-facing base classes that mix in more than one 
Scala trait.
+ *
+ * <p>See {@code org.apache.pekko.actor.JavaSubclassCompilationCheck} for why 
declaring the subclass
+ * is the whole test. {@link AbstractPersistentLoggingFSM} has no other Java 
subclass in the build.
+ *
+ * <p>The class under guard is deprecated, so the subclass is marked 
deprecated too: javac's test
+ * configuration runs with `-Werror`, and a use of deprecated API inside a 
deprecated element does
+ * not warn. This mirrors {@code AbstractPersistentFSMTest}.
+ */
+public final class JavaSubclassCompilationCheck {
+
+  private JavaSubclassCompilationCheck() {}
+
+  @Deprecated
+  abstract static class PersistentLoggingFSM
+      extends AbstractPersistentLoggingFSM<PersistentFSM.FSMState, String, 
String> {}
+}
diff --git 
a/persistence/src/test/scala/org/apache/pekko/persistence/TimerPersistentActorSpec.scala
 
b/persistence/src/test/scala/org/apache/pekko/persistence/TimerPersistentActorSpec.scala
index b912761755..97d114a6a3 100644
--- 
a/persistence/src/test/scala/org/apache/pekko/persistence/TimerPersistentActorSpec.scala
+++ 
b/persistence/src/test/scala/org/apache/pekko/persistence/TimerPersistentActorSpec.scala
@@ -111,6 +111,12 @@ class TimerPersistentActorSpec extends 
PersistenceSpec(ConfigFactory.parseString
       expectMsg("msg2")
     }
 
+    "not discard timer msg due to stashing for a Java subclass of 
AbstractPersistentActorWithTimers" in {
+      val pa = system.actorOf(Props(classOf[JavaTimerPersistentActor], "p4"))
+      pa ! "msg4"
+      expectMsg("msg4")
+    }
+
     "reject wrong order of traits, PersistentActor with Timer" in {
       if (TraitOrder.canBeChecked) {
         val pa = system.actorOf(Props[WrongOrder]())
diff --git 
a/stream-tests/src/test/java/org/apache/pekko/stream/stage/JavaSubclassCompilationCheck.java
 
b/stream-tests/src/test/java/org/apache/pekko/stream/stage/JavaSubclassCompilationCheck.java
new file mode 100644
index 0000000000..6745314c69
--- /dev/null
+++ 
b/stream-tests/src/test/java/org/apache/pekko/stream/stage/JavaSubclassCompilationCheck.java
@@ -0,0 +1,32 @@
+/*
+ * 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.stream.stage;
+
+/**
+ * Compile-only guard for Java-facing base classes that mix in more than one 
Scala trait.
+ *
+ * <p>See {@code org.apache.pekko.actor.JavaSubclassCompilationCheck} for why 
declaring the subclass
+ * is the whole test. {@link AbstractInOutHandler} mixes in both {@code 
InHandler} and {@code
+ * OutHandler} and has no other Java subclass in the build.
+ */
+public final class JavaSubclassCompilationCheck {
+
+  private JavaSubclassCompilationCheck() {}
+
+  abstract static class InOutHandler extends AbstractInOutHandler {}
+}


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

Reply via email to