Skip to content

fix: make AbstractPersistentActorWithTimers and AbstractFSMWithStash subclassable from Java on Scala 3 - #3475

Merged
pjfanning merged 3 commits into
apache:mainfrom
pjfanning:fix-java-subclassing-scala3-mixins
Aug 27, 2026
Merged

fix: make AbstractPersistentActorWithTimers and AbstractFSMWithStash subclassable from Java on Scala 3#3475
pjfanning merged 3 commits into
apache:mainfrom
pjfanning:fix-java-subclassing-scala3-mixins

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

Extending AbstractPersistentActorWithTimers from Java fails to compile against the Scala 3 artifacts:

TestActor.java:[5,8] types org.apache.pekko.actor.Timers and org.apache.pekko.persistence.Eventsourced are incompatible;
  class TestActor inherits unrelated defaults for aroundPreRestart(java.lang.Throwable,scala.Option<java.lang.Object>)
  from types org.apache.pekko.actor.Timers and org.apache.pekko.persistence.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:

public void aroundPreRestart(java.lang.Throwable, scala.Option);
  flags: (0x1041) ACC_PUBLIC, 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. This is accidental, not a deliberate limitation.

The reporter asked whether other multi-trait base classes are affected. I compiled a javac probe (an abstract Java subclass) against the Scala 3 build output for every Java-facing Abstract*/UntypedAbstract* class that mixes in more than one trait — the stash, logging and timers variants, AbstractLoggingFSM, AbstractFSMWithStash, AbstractPersistentActor, AbstractPersistentActorWithTimers, AbstractPersistentActorWithAtLeastOnceDelivery, AbstractPersistentFSM, AbstractPersistentLoggingFSM, journal.japi.AsyncWriteJournal, AbstractInOutHandler and AbstractSerializationSupport. Exactly one other class has the same defect: AbstractFSMWithStash, where FSM and UnrestrictedStash both implement postStop.

Modification

Give both classes real (non-synthetic) overrides of the conflicting members that simply delegate to super, which resolves exactly like the forwarders they replace, so behaviour is unchanged on both Scala versions.

AbstractFSMWithStash.postStop deliberately carries no @throws(classOf[Exception]): FSM.postStop declares no checked exceptions, and a wider throws clause is not a valid override for javac.

Added Java sources exercising both classes, so the Scala 3 build fails if this regresses:

  • persistence/src/test/java/.../JavaTimerPersistentActor.java, driven by a new case in TimerPersistentActorSpec
  • actor-tests/src/test/java/.../AbstractFSMWithStashActorTest.java, a JUnit 5 test

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" (2.13) — 5 succeeded, 0 failed
  • sbt "actor-tests/testOnly org.apache.pekko.actor.AbstractFSMWithStashActorTest org.apache.pekko.actor.AbstractFSMActorTest" (2.13) — 2 succeeded, 0 failed
  • sbt headerCreateAll javafmtAll scalafmtAll scalafmtSbt on JDK 17 — headers generated by sbt, no formatting churn outside the changed files
  • sbt +mimaReportBinaryIssues — Not run locally, left to the CI Check / Binary Compatibility job

Both Java test sources fail to compile under Scala 3 without the corresponding production change, and compile once it is applied.

References

Fixes #3474

…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 apache#3474
@pjfanning
pjfanning merged commit 90b02d6 into apache:main Aug 27, 2026
10 checks passed
@pjfanning
pjfanning deleted the fix-java-subclassing-scala3-mixins branch August 27, 2026 12:00
pjfanning added a commit to pjfanning/incubator-pekko that referenced this pull request Aug 27, 2026
Motivation:
The cherry-pick of apache#3475 does not compile on 1.7.x. That branch has an older
API surface than main:

- `actor-tests` has no JUnit 5; `PekkoJUnitJupiterActorSystemResource` and the
  `org.junit.jupiter` packages do not exist there.
- `UntypedAbstractLoggingActor`, `UntypedAbstractActorWithStash`,
  `UntypedAbstractActorWithUnboundedStash` and
  `UntypedAbstractActorWithUnrestrictedStash` do not exist on 1.7.x.

Modification:
Port `AbstractFSMWithStashActorTest` to JUnit 4 with `JUnitSuite` and
`PekkoJUnitActorSystemResource`, matching the neighbouring
`AbstractFSMActorTest`.

Drop `actor-tests/.../JavaSubclassCompilationCheck.java`. Every Java-facing
class on 1.7.x that mixes in more than one trait already has a Java subclass
somewhere in the build, so the guard has nothing left to cover on this branch.
The persistence and stream guards still apply and are unchanged.

Result:
The backport compiles and its tests pass on 1.7.x.

Tests:
- sbt "actor-tests/Test/compile" "persistence/Test/compile" "stream-tests/Test/compile" - success
- sbt "actor-tests/testOnly org.apache.pekko.actor.AbstractFSMWithStashActorTest" - 1 succeeded, 0 failed
- sbt "persistence/testOnly org.apache.pekko.persistence.TimerPersistentActorSpec" - 5 succeeded, 0 failed
- sbt javafmtAll (JDK 17) - no churn outside the changed files
- Scala 2.12 and Scala 3 - Not run locally, left to CI

References:
Refs apache#3475, Refs apache#3474
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AbstractPersistentActorWithTimers is not useable from Java

2 participants