Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Cross-compile Akka with Scala 3 #29929

Closed
wants to merge 6 commits into from
Closed

Conversation

andreaTP
Copy link
Contributor

@andreaTP andreaTP commented Jan 5, 2021

This PR aims to kick start the work and conversations on cross-compiling Akka with, upcoming, Scala 3.

Current status:
Modules akka-actor, akka-testkit, akka-actor-tests are compiling and all of the tests are passing locally (but TypedMultiMapSpec which is commented) starting sbt with sbt -Dakka.build.scalaVersion=3.0.
Started cross-compiling akka-stream which is down to ~50 compile-time errors, but I have a problem with variance (i.e. @uncheckedVariance) that needs further attention.

There are a number of small changes that are simple refactorings and I will follow up with separate PRs to current master to reduce the diff (e.g. extra import to enable the bang operator).

I'll comment on the diff a bunch of additional points that came out and try to ping relevant ppl for help 馃檹

Note: This is a personal initiative started during the Christmas break 馃檪

@akka-ci
Copy link

akka-ci commented Jan 5, 2021

Thank you for your pull request! After a quick sanity check one of the team will reply with 'OK 韦O 韦ES韦' to kick off our automated validation on Jenkins. This compiles the project, runs the tests, and checks for things like binary compatibility and source code formatting. When two team members have also manually reviewed and (perhaps after asking for some amendments) accepted your contribution, it should be good to be merged.

For more details about our contributing process, check out CONTRIBUTING.md - and feel free to ask!

@@ -16,6 +16,9 @@ import akka.actor._
*/
class AkkaExceptionSpec extends AnyWordSpec with Matchers {

// TODO DOTTY, yes I know it's wrong
implicit val pos: org.scalactic.source.Position = new org.scalactic.source.Position(fileName = "", filePathname = "", lineNumber = 1)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a workaround, ScalaTest is usable with Scala 3, but I found a few rough edges.
The implicit position is provided from a macro in Scala 2.X and is not available in Scala 3.

I see that there is some ongoing work to cross-compile ScalaTest (e.g. scalatest/scalatest@e8201d4 ) and I expect that this issue is solved there, maybe @bvenners or @cheeseng can chip in?

Reference issue:
scalatest/scalatest#1950

implicit val pos: org.scalactic.source.Position = new org.scalactic.source.Position(fileName = "", filePathname = "", lineNumber = 1)

// TODO DOTTY
protected override def runTest(testName: String, args: org.scalatest.Args): org.scalatest.Status = akka.testkit.ScalatestRunTest.scalatestRunTest(testName, args)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is again regarding ScalaTest, I would love to know if there is a better way of doing it.
cc. @bvenners @cheeseng

@@ -466,8 +466,8 @@ class ActorRefSpec extends AkkaSpec("""
def receive = { case name: String => sender() ! context.child(name).isDefined }
}), "parent")

assert(Await.result((parent ? "child"), timeout.duration) === true)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert is a macro and is not usable "as is", for the moment I switched to require, but I was expecting this to work out of the box, maybe @nicolasstucki or @smarter can shed a light?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the issue with assert?

Copy link
Contributor Author

@andreaTP andreaTP Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since assert is a macro Scala 3 rejects it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolasstucki there's no actual issue, this project is just using a scala 2 version of scalatest instead of the scala 3 version: #29929 (comment)

@@ -51,7 +59,7 @@ class DynamicAccessSpec extends AnyWordSpec with Matchers with BeforeAndAfterAll
instantiateWithDefaultOrStringCtor("akka.actor.TestClassWithDefaultConstructor").get.name shouldBe "default"
instantiateWithDefaultOrStringCtor("akka.actor.foo.NonExistingClass") match {
case Failure(t) =>
t shouldBe a[ClassNotFoundException]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScalaTest related type check is done via a macro.
cc. @bvenners @cheeseng

}

}
// "A TypedMultiMap" must {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that TypedMultiMap types signatures are pretty bound to Scala 2, anyone I can ping on the subject?
maybe @raboof or @johanandren can point me to the right person?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's our own impl so not sure anyone is more or less suitable to say anything about it without looking at in which way it does not work on Scala 3. Perhaps it would be possible to implement a source compatible version specifically for Scala 3?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a look and give it a spin.

ActorCell.class.getDeclaredField(
"akka$actor$dungeon$Dispatch$$_mailboxDoNotCallMeDirectly"));
// TODO DOTTY
ActorCell.class.getDeclaredField("_mailboxDoNotCallMeDirectly"));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I need some guidance on how to proceed, this AFAIU prevents today to use Akka Actor in a Scala 3 project.
But, at the same time, rely on the internals of the compiler, should we implement fallbacks or tackle the problem differently? cc. @raboof @johanandren @patriknw

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that @eloots was using exclusively Akka Typed, probably not hitting the untyped ActorCell. But this needs to be proved ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offsets here is used by the underlying impl also for typed actors (those are on top of "classic" actors), so that should be involved regardless. But Erics sample just used the pre-compiled 2.13 Akka artifacts rather than compiling them with Scala 3 himself, so maybe that keeps the encoding.

I guess this would have to be separate logic for Scala 2 and 3 somehow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll double-check that this is needed after the rest of the more "trivial" changes get merged, in case I'll go for the switch case. (Do we already have available the scalaVersion in the runtime, such as BuildInfo or something like Version?)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither Scala 2 nor Scala 3 give any guarantee about the naming scheme of private fields, so while this might work on a particular version of a compiler, it might not work on the next.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand there is no such guarantee/promise, it has been stable in Scala 2 at least since 2012 (and Scala 2.10.0-M7). This will likely be tricky to solve if the internal encoding in Scala 3 is expected to change often.

Perhaps moving on to VarHandle from Unsafe could solve it, but we can't do that as long as we also support Java 8 (I have a feeling that there was performance concerns around that as well).

@SerialVersionUID(1L)
// TODO DOTTY
// @SerialVersionUID does nothing on a trait
// @SerialVersionUID(1L)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we add an annotation and the silencer to prevent the compiler to say that it's not going to work, any help @raboof @johanandren ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both the concrete subclasses have their own @serialversionuid, I think it is completely fine to remove here as it doesn't do anything anyway?

@@ -128,7 +128,10 @@ class LightArrayRevolverScheduler(config: Config, log: LoggingAdapter, threadFac
}
}

@tailrec final def cancel(): Boolean = {
// TODO DOTTY
// Cannot rewrite recursive call: it targets a supertype
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm puzzled by this error message, is it the expected behavior @nicolasstucki, @smarter ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message refers toc.cancel() which is an overridden method of the current cancel() method, dotty correctly tells you that it cannot rewrite it, scala 2 doesn't say anything here, it's not a bug, just a difference of what @tailrec checks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in #29939

@silent @volatile private var _cellDoNotCallMeDirectly: Cell = _
@silent @volatile private var _lookupDoNotCallMeDirectly: Cell = _
// TODO DOTTY
@silent @volatile protected var _cellDoNotCallMeDirectly: Cell = _
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a real "issue", e.g. something that is ok for a POC but is not going to be merged on master.

My understanding is that private unused variables are erased by the Scala 3 compiler, is there a way to preserve them(with an annotation or something) since in this case they are used entirely through Unsafe?

cc. @nicolasstucki @smarter

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a def: ohze@26394d4

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in #29930

final case class Envelope private (message: Any, sender: ActorRef)
final case class Envelope private (message: Any, sender: ActorRef) {
// TODO DOTTY -> bug? copy method becomes private with private constructor
def copy(message: Any, sender: ActorRef) = Envelope(message, sender)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Dotty the copy method of case classes have the same visibility as the constructor AFAIU.
Is it a deliberate change from Scala 2 to Scala 3 or is it accidental? @nicolasstucki @smarter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @SethTisue ! Means we have to declare them explicitly.

@@ -15,7 +15,8 @@ import akka.util.unused
*
* Watches all actors which subscribe on the given event stream, and unsubscribes them from it when they are Terminated.
*/
protected[akka] class ActorClassificationUnsubscriber(bus: ManagedActorClassification, debug: Boolean)
// TODO DOTTY -> Something wrong is happening in the compiler
protected[akka] class ActorClassificationUnsubscriber(bus: String, unsubscribe: ActorRef => Unit, debug: Boolean)
Copy link
Contributor Author

@andreaTP andreaTP Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why, but the compiler go completely nuts on this detail, the pattern is something like:
file1.scala

class A {
  val b = new B(this)
  def foo() = {}
}

file2.scala

class B(a: A) {
  def bar() = {
    a.foo()
  }
}

@nicolasstucki @smarter

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not enough details for us to say anything (what does "completely nuts" mean?).

Copy link
Contributor Author

@andreaTP andreaTP Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiler rejects the build saying that one of the class file is missing.
It builds although using incremental compilation by removing dependencies on the other compilation unit and re-adding them back.
I can try to to provide a branch as a repro, sorry for the lack of details.

block: function.Function2[GraphDSL.Builder[M], S1, S]): Graph[S, M] =
scaladsl.GraphDSL.create(g1) { b => s => block.apply(b.asJava, s) }
scaladsl.GraphDSL.create(g1) { b => (s: Shape @uncheckedVariance) => block.apply(b.asJava, s.asInstanceOf[S1]) }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this solves a bunch of errors but is probably wrong

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, when Scala 3 requires @uncheckedVariance where Scala 2 doesn't, it's entirely plausible that the Scala 2 compiler was wrongly accepting unsound code. (But that's in general; I haven't dug into this particular case.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original sin here is the definition of Shape with @uncheckedVariance:

type Shape = S @uncheckedVariance

This breaks soundness as demonstrated by https://scastie.scala-lang.org/smarter/Cno5sLZyTSybDC80uDt2hQ, meaning that we can't really guarantee anything about type-safety in akka.

@@ -23,7 +23,22 @@ import akka.pattern.ask
* @since 1.1
*/
@silent // 'early initializers' are deprecated on 2.13 and will be replaced with trait parameters on 2.14. https://github.com/akka/akka/issues/26753
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this implementation to the scala-3 folder and use proper trait parameters in Scala 3?
@raboof @johanandren

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that works with minimal changes it sound fine by me

@@ -64,10 +64,14 @@ object AkkaDisciplinePlugin extends AutoPlugin {
"akka-testkit")

lazy val silencerSettings = {
val silencerVersion = "1.7.1"
val silencerVersion = "1.5-SNAPSHOT"
// TODO: FIXME DOTTY (using a locally published version of silencer...)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simply published locally an "empty version" of the silencer, any plan to port it to Scala 3 already on-going @ghik ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Scala 3 support @nowarn? I would expect that it does. All @silent annotations should be migrated to @nowarn - silencer would then be necessary only in Scala 2.12 (it understands @nowarn to some extent).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ghik for the answer!
The question then goes to the Akka Team ( @johanandren @raboof ): can we migrate all the annotations to @nowarn in the entire code-base, and tweak compiler flags to be more lenient in Scala 2.12?
Or, we can define a local annotation @nowarn in the scala-212 folder that simply rebind silence (have to test this option).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nowarn is already available for Scala 2.12 in scala-collection-compat. It is partially interpreted by silencer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nowarn will be in Scala 2.12.13 (scala/scala#9248), so I think it'd make sense to wait for that.

Do we need to make our flags more lenient in order to move to @nowarn?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to make our flags more lenient in order to move to @nowarn?

Don't know, will test!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nowarn is not implemented in Scala 3 yet: scala/scala3#8908


val lmdb = "org.lmdbjava" % "lmdbjava" % "0.7.0" // ApacheV2, OpenLDAP Public License

val junit = "junit" % "junit" % junitVersion // Common Public License 1.0

// For Java 8 Conversions
val java8Compat = Def.setting { "org.scala-lang.modules" %% "scala-java8-compat" % java8CompatVersion.value } // Scala License
// TODO DOTTY
val java8Compat = Def.setting { DottyCompatModuleID("org.scala-lang.modules" %% "scala-java8-compat" % java8CompatVersion.value).withDottyCompat(getVersion()) } // Scala License
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a library that probably we want to, eventually, cross-compile as well for Scala 3, cc. @SethTisue

Copy link
Contributor

@SethTisue SethTisue Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, scala/scala-java8-compat#200 ... we're hoping someone will volunteer to tackle it. if you have any problems using it with withDottyCompat, let me know, I would consider that high priority to investigate

val scalatest = "org.scalatest" %% "scalatest" % scalaTestVersion % "test" // ApacheV2
val scalacheck = "org.scalacheck" %% "scalacheck" % scalaCheckVersion % "test" // New BSD
// TODO DOTTY
val scalatest = DottyCompatModuleID("org.scalatest" %% "scalatest" % scalaTestVersion % "test").withDottyCompat(getVersion()) // ApacheV2
Copy link

@smarter smarter Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All your problems related to scalatest stem from the fact that you're using the Scala 2 version of scalatest, so none of the macros are usable. You should drop the withDottyCompat for all dependencies which are published for Scala 3, this includes scalatest: https://repo1.maven.org/maven2/org/scalatest/scalatest_3.0.0-M3/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing didn't know about it! Thanks a lot @smarter !
Unfortunately, Scala 3 is not listed by Maven central:
https://mvnrepository.com/artifact/org.scalatest/scalatest

How do we know if a library is available or we should use the compatibility mode?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scaladex can help: https://index.scala-lang.org/scalatest/scalatest/scalatest/3.2.3 (see the "version matrix" button on the right), or just browse https://repo1.maven.org/maven2 manually.

Copy link
Contributor Author

@andreaTP andreaTP Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Scala 3.0.0-M3 still missing in Scaladex?
And, found that, probably, this is a good starting point:
https://index.scala-lang.org/search?q=depends-on:lampepfl/scala3-library&page=1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in #29930

@smarter
Copy link

smarter commented Jan 5, 2021

FYI, there's a wip dotty branch for akka in https://github.com/ohze/akka/commits/dotty/wip by @ohze / @giabao, you might want to collaborate.

@patriknw
Copy link
Member

Superseded by #29956, I assume

@patriknw patriknw closed this Jan 25, 2021
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.

None yet

9 participants