From 12456d6ba002a196f0525365b23ec2e3a94e7ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Wed, 21 Feb 2024 17:58:20 +0100 Subject: [PATCH 1/6] native image with h2 inmem passing locally --- .github/workflows/native-image-tests.yml | 10 +- .../reflect-config.json | 40 ++++++ .../netty-handler/native-image.properties | 1 + native-image-tests/.gitignore | 12 ++ native-image-tests/build.sbt | 34 +++++ native-image-tests/project/build.properties | 1 + native-image-tests/project/plugins.sbt | 1 + .../resources/META-INF/native-image/README.md | 1 + .../logback-classic/reflect-config.json | 65 ++++++++++ .../native-image.properties | 1 + .../native-image-tests/resource-config.json | 8 ++ .../src/main/resources/application.conf | 10 ++ .../src/main/resources/logback.xml | 13 ++ .../main/scala/com/lightbend/EsbTester.scala | 121 ++++++++++++++++++ .../src/main/scala/com/lightbend/Main.scala | 33 +++++ 15 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json create mode 100644 core/src/main/resources/META-INF/native-image/io.netty/netty-handler/native-image.properties create mode 100644 native-image-tests/.gitignore create mode 100644 native-image-tests/build.sbt create mode 100644 native-image-tests/project/build.properties create mode 100644 native-image-tests/project/plugins.sbt create mode 100644 native-image-tests/src/main/resources/META-INF/native-image/README.md create mode 100644 native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json create mode 100644 native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties create mode 100644 native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json create mode 100644 native-image-tests/src/main/resources/application.conf create mode 100644 native-image-tests/src/main/resources/logback.xml create mode 100644 native-image-tests/src/main/scala/com/lightbend/EsbTester.scala create mode 100644 native-image-tests/src/main/scala/com/lightbend/Main.scala diff --git a/.github/workflows/native-image-tests.yml b/.github/workflows/native-image-tests.yml index a9a3fc37..bf1588cb 100644 --- a/.github/workflows/native-image-tests.yml +++ b/.github/workflows/native-image-tests.yml @@ -47,12 +47,12 @@ jobs: run: |- sbt "publishLocal" - #- name: Akka Persistence R2DBC native image test app build - # run: |- - # cd native-image-tests/ - # sbt nativeImage -Dakka.http.version=`cat ~/.version` + - name: Akka Persistence R2DBC native image test app build + run: |- + cd native-image-tests/ + sbt nativeImage -Dakka.r2dbc.version=`cat ~/.version` # run the binary - # target/native-image/native-image-tests + target/native-image/native-image-tests - name: Email on failure if: ${{ failure() }} diff --git a/core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json b/core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json new file mode 100644 index 00000000..65eb2451 --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json @@ -0,0 +1,40 @@ +[ + { + "name": "akka.persistence.r2dbc.journal.R2dbcJournal", + "methods": [ + { + "name": "", + "parameterTypes": [ + "com.typesafe.config.Config", + "java.lang.String" + ] + } + ] + }, + { + "name": "akka.persistence.r2dbc.query.R2dbcReadJournalProvider", + "methods": [ + { + "name": "", + "parameterTypes": [ + "akka.actor.ExtendedActorSystem", + "com.typesafe.config.Config", + "java.lang.String" + ] + } + ] + }, + { + "name": "akka.persistence.r2dbc.snapshot.R2dbcSnapshotStore", + "methods": [ + { + "name": "", + "parameterTypes": [ + "com.typesafe.config.Config", + "java.lang.String" + ] + } + ] + } + +] \ No newline at end of file diff --git a/core/src/main/resources/META-INF/native-image/io.netty/netty-handler/native-image.properties b/core/src/main/resources/META-INF/native-image/io.netty/netty-handler/native-image.properties new file mode 100644 index 00000000..755a25a1 --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/io.netty/netty-handler/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils \ No newline at end of file diff --git a/native-image-tests/.gitignore b/native-image-tests/.gitignore new file mode 100644 index 00000000..c4ddf444 --- /dev/null +++ b/native-image-tests/.gitignore @@ -0,0 +1,12 @@ +target/ + +.settings +.project +.classpath + +.idea +*.iml + +.metals +.bloop +.bsp diff --git a/native-image-tests/build.sbt b/native-image-tests/build.sbt new file mode 100644 index 00000000..58f1ff8b --- /dev/null +++ b/native-image-tests/build.sbt @@ -0,0 +1,34 @@ +name := "native-image-tests" + +version := "1.0" + +scalaVersion := "2.13.12" + +resolvers += "Akka library repository".at("https://repo.akka.io/maven") + +lazy val akkaVersion = sys.props.getOrElse("akka.version", "2.9.1") +lazy val akkaR2dbcVersion = sys.props.getOrElse("akka.r2dbc.version", "1.2.2") + +// Run in a separate JVM, to make sure sbt waits until all threads have +// finished before returning. +// If you want to keep the application running while executing other +// sbt tasks, consider https://github.com/spray/sbt-revolver/ +fork := true + +// GraalVM native image build +enablePlugins(NativeImagePlugin) +nativeImageJvm := "graalvm-community" +nativeImageVersion := "21.0.2" +nativeImageOptions := Seq("--no-fallback", "--verbose", "-Dakka.native-image.debug=true") + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-persistence-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-persistence-query" % akkaVersion, + "com.typesafe.akka" %% "akka-slf4j" % akkaVersion, + "com.typesafe.akka" %% "akka-serialization-jackson" % akkaVersion, + "com.lightbend.akka" %% "akka-persistence-r2dbc" % akkaR2dbcVersion, + "ch.qos.logback" % "logback-classic" % "1.2.13", + // H2 + "com.h2database" % "h2" % "2.2.224", + "io.r2dbc" % "r2dbc-h2" % "1.0.0.RELEASE") diff --git a/native-image-tests/project/build.properties b/native-image-tests/project/build.properties new file mode 100644 index 00000000..abbbce5d --- /dev/null +++ b/native-image-tests/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.8 diff --git a/native-image-tests/project/plugins.sbt b/native-image-tests/project/plugins.sbt new file mode 100644 index 00000000..f28fde7a --- /dev/null +++ b/native-image-tests/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") diff --git a/native-image-tests/src/main/resources/META-INF/native-image/README.md b/native-image-tests/src/main/resources/META-INF/native-image/README.md new file mode 100644 index 00000000..6a176a67 --- /dev/null +++ b/native-image-tests/src/main/resources/META-INF/native-image/README.md @@ -0,0 +1 @@ +Custom config here is for logging using logback, loading keystore from classpath for TLS tests diff --git a/native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json b/native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json new file mode 100644 index 00000000..1b630ce7 --- /dev/null +++ b/native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json @@ -0,0 +1,65 @@ +[ + { + "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.DateConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LevelConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LineSeparatorConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.LoggerConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.MDCConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.MarkerConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.MessageConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.classic.pattern.ThreadConverter", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.ConsoleAppender", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"ch.qos.logback.core.OutputStreamAppender", + "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] + }, + { + "name":"ch.qos.logback.core.encoder.Encoder", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] + }, + { + "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] + }, + { + "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] + }, + { + "name":"ch.qos.logback.core.spi.ContextAware", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] + } + +] diff --git a/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties b/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties new file mode 100644 index 00000000..3dea9206 --- /dev/null +++ b/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=org.slf4j.LoggerFactory,ch.qos.logback,org.slf4j.impl.StaticLoggerBinder,org.slf4j.MDC diff --git a/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json b/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json new file mode 100644 index 00000000..e70119bc --- /dev/null +++ b/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json @@ -0,0 +1,8 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qlogback.xml\\E" + }] + }, + "bundles":[] +} diff --git a/native-image-tests/src/main/resources/application.conf b/native-image-tests/src/main/resources/application.conf new file mode 100644 index 00000000..c20941bf --- /dev/null +++ b/native-image-tests/src/main/resources/application.conf @@ -0,0 +1,10 @@ +akka.persistence.journal.plugin = "akka.persistence.r2dbc.journal" +akka.persistence.snapshot-store.plugin = "akka.persistence.r2dbc.snapshot" +akka.persistence.state.plugin = "akka.persistence.r2dbc.state" + +akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.h2} +akka.persistence.r2dbc.connection-factory = { + # overrides for default values from the 'akka.persistence.r2dbc.h2' config block + protocol = "mem" + database = "mydb" +} \ No newline at end of file diff --git a/native-image-tests/src/main/resources/logback.xml b/native-image-tests/src/main/resources/logback.xml new file mode 100644 index 00000000..4b6e3608 --- /dev/null +++ b/native-image-tests/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + [%date{ISO8601}] [%level] [%logger] [%X{akkaAddress}] [%marker] [%thread] - %msg%n + + + + + + + + diff --git a/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala b/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala new file mode 100644 index 00000000..35658355 --- /dev/null +++ b/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2009-2024 Lightbend Inc. + */ +package com.lightbend + +import akka.actor.typed.ActorRef +import akka.actor.typed.Behavior +import akka.actor.typed.scaladsl.Behaviors +import akka.pattern.StatusReply +import akka.persistence.typed.PersistenceId +import akka.persistence.typed.scaladsl.Effect +import akka.persistence.typed.scaladsl.EventSourcedBehavior +import akka.serialization.jackson.JsonSerializable +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +import scala.concurrent.duration.DurationInt + +object EventSourcedCounter { + sealed trait Command extends JsonSerializable + + final case class Increase(amount: Int, replyTo: ActorRef[StatusReply[Increased]]) extends Command + @JsonCreator + final case class GetValue(replyTo: ActorRef[StatusReply[GetValueResponse]]) extends Command + final case class GetValueResponse(value: Int) + + sealed trait Event extends JsonSerializable + + // FIXME why doesn't @JsonCreator work as usual? is it something missing from the akka jackson feature? + final case class Increased(@JsonProperty("amount") amount: Int) extends Event + + final case class State(@JsonProperty("value") value: Int) extends JsonSerializable + + def apply(id: String): Behavior[Command] = EventSourcedBehavior[Command, Event, State]( + PersistenceId("EventSourcedHelloWorld", id), + State(0), + { (state, command) => + command match { + case Increase(increment, replyTo) => + val increased = Increased(increment) + Effect.persist(increased).thenReply(replyTo)(_ => StatusReply.success(increased)) + case GetValue(replyTo) => + Effect.reply(replyTo)(StatusReply.success(GetValueResponse(state.value))) + } + }, + { (_, event) => + event match { + case Increased(newGreeting) => State(newGreeting) + } + }).snapshotWhen((_, _, seqNr) => seqNr % 2 == 0) +} + +object EsbTester { + + object EsbStopped + + def apply(whenDone: ActorRef[String]): Behavior[AnyRef] = Behaviors.setup { context => + Behaviors.withTimers { timers => + + timers.startSingleTimer("Timeout", 10.seconds) + + var eventSourcedHelloWorld = context.spawn(EventSourcedCounter("one"), "EsbOne") + context.watchWith(eventSourcedHelloWorld, EsbStopped) + eventSourcedHelloWorld ! EventSourcedCounter.Increase(1, context.self) + + def messageOrTimeout(step: String)(partial: PartialFunction[AnyRef, Behavior[AnyRef]]): Behavior[AnyRef] = { + context.log.info("On {}", step) + Behaviors.receiveMessage(message => + partial.orElse[AnyRef, Behavior[AnyRef]] { + case "Timeout" => + context.log.error(s"ESB checks timed out in {}", step) + System.exit(1) + Behaviors.same + + case other => + context.log.warn("Unexpected message in {}: {}", step, other) + Behaviors.same + }(message)) + } + + def step1() = messageOrTimeout("step1") { case StatusReply.Success(EventSourcedCounter.Increased(1)) => + eventSourcedHelloWorld ! EventSourcedCounter.Increase(2, context.self) + step2() + } + + def step2() = + messageOrTimeout("step2") { case StatusReply.Success(EventSourcedCounter.Increased(2)) => + // triggers snapshot + eventSourcedHelloWorld ! EventSourcedCounter.Increase(2, context.self) + step3() + } + + def step3() = + messageOrTimeout("step3") { case StatusReply.Success(EventSourcedCounter.Increased(2)) => + eventSourcedHelloWorld ! EventSourcedCounter.GetValue(context.self) + step4() + } + + def step4() = messageOrTimeout("step4") { case StatusReply.Success(EventSourcedCounter.GetValueResponse(2)) => + context.stop(eventSourcedHelloWorld) + step5() + } + + def step5() = messageOrTimeout("step5") { case EsbStopped => + // start anew to trigger replay + eventSourcedHelloWorld = context.spawn(EventSourcedCounter("one"), "EsbOneIncarnation2") + eventSourcedHelloWorld ! EventSourcedCounter.GetValue(context.self) + step6() + + } + + def step6() = messageOrTimeout("step6") { case StatusReply.Success(EventSourcedCounter.GetValueResponse(2)) => + // replay was fine + whenDone ! "ESB works" + Behaviors.stopped + } + + step1() + } + } +} diff --git a/native-image-tests/src/main/scala/com/lightbend/Main.scala b/native-image-tests/src/main/scala/com/lightbend/Main.scala new file mode 100644 index 00000000..767439d7 --- /dev/null +++ b/native-image-tests/src/main/scala/com/lightbend/Main.scala @@ -0,0 +1,33 @@ +package com.lightbend + +import akka.actor.typed.ActorSystem +import akka.actor.typed.Behavior +import akka.actor.typed.scaladsl.Behaviors + +object RootBehavior { + def apply(): Behavior[AnyRef] = Behaviors.setup { context => + context.spawn(EsbTester(context.self), "ESBTester") + + var awaitedOks = Set("ESB works") + + Behaviors.receiveMessage { + case string: String => + awaitedOks -= string + if (awaitedOks.isEmpty) { + context.log.info("All checks ok, shutting down") + Behaviors.stopped + } else { + Behaviors.same + } + case other => + context.log.warn("Unexpected message: {}", other) + Behaviors.same + } + } +} + +object Main extends App { + + ActorSystem(RootBehavior(), "R2dbcTester") + +} From c003f7897dfd2577971ea62e45e1ec14602fcfec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Fri, 23 Feb 2024 17:22:29 +0100 Subject: [PATCH 2/6] Working for H2 and event source, snapshot, durable state --- .../reflect-config.json | 13 +++ .../com/lightbend/DurableStateTester.scala | 86 +++++++++++++++++++ .../main/scala/com/lightbend/EsbTester.scala | 9 +- .../src/main/scala/com/lightbend/Main.scala | 38 +++++--- 4 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala diff --git a/core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json b/core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json index 65eb2451..6e247736 100644 --- a/core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json +++ b/core/src/main/resources/META-INF/native-image/com.lightbend.akka/akka-persistence-r2dbc/reflect-config.json @@ -35,6 +35,19 @@ ] } ] + }, + { + "name": "akka.persistence.r2dbc.state.R2dbcDurableStateStoreProvider", + "methods": [ + { + "name": "", + "parameterTypes": [ + "akka.actor.ExtendedActorSystem", + "com.typesafe.config.Config", + "java.lang.String" + ] + } + ] } ] \ No newline at end of file diff --git a/native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala b/native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala new file mode 100644 index 00000000..6017f71a --- /dev/null +++ b/native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2009-2024 Lightbend Inc. + */ +package com.lightbend + +import akka.actor.typed.ActorRef +import akka.actor.typed.Behavior +import akka.actor.typed.scaladsl.Behaviors +import akka.persistence.typed.PersistenceId +import akka.persistence.typed.state.scaladsl.DurableStateBehavior +import akka.persistence.typed.state.scaladsl.Effect +import akka.serialization.jackson.JsonSerializable +import com.fasterxml.jackson.annotation.JsonCreator + +import scala.concurrent.duration.DurationInt + +object DurableStateCounter { + sealed trait Command extends JsonSerializable + final case class Increase(amount: Int, replyTo: ActorRef[Increased]) extends Command + + // FIXME why doesn't @JsonCreator work as usual? is it something missing from the jackson feature? + final case class GetState @JsonCreator() (replyTo: ActorRef[State]) extends Command + + final case class Increased @JsonCreator() (newValue: Int) extends JsonSerializable + + final case class State @JsonCreator() (value: Int) extends JsonSerializable + def apply(id: String): Behavior[Command] = + DurableStateBehavior[Command, State]( + PersistenceId("DSCounter", id), + State(0), + { + case (state, Increase(amount, replyTo)) => + Effect.persist(State(state.value + amount)).thenReply(replyTo)(newState => Increased(newState.value)) + case (state, GetState(replyTo)) => + Effect.reply(replyTo)(state) + }) +} + +object DurableStateTester { + + def apply(whenDone: ActorRef[String]): Behavior[AnyRef] = Behaviors.setup { context => + Behaviors.withTimers { timers => + timers.startSingleTimer("Timeout", 10.seconds) + + var durableActor = context.spawn(DurableStateCounter("one"), "DurableOne") + context.watchWith(durableActor, "DurableOneStopped") + + def messageOrTimeout(step: String)(partial: PartialFunction[AnyRef, Behavior[AnyRef]]): Behavior[AnyRef] = { + context.log.info("On {}", step) + Behaviors.receiveMessage(message => + partial.orElse[AnyRef, Behavior[AnyRef]] { + case "Timeout" => + context.log.error(s"Durable state checks timed out in {}", step) + System.exit(1) + Behaviors.same + + case other => + context.log.warn("Unexpected message in {}: {}", step, other) + Behaviors.same + }(message)) + } + + durableActor ! DurableStateCounter.Increase(1, context.self) + + def step1() = messageOrTimeout("step1") { case DurableStateCounter.Increased(1) => + // write works + context.stop(durableActor) + step2() + } + + def step2() = messageOrTimeout("step2") { case "DurableOneStopped" => + durableActor = context.spawn(DurableStateCounter("one"), "DurableOneIncarnation2") + durableActor ! DurableStateCounter.GetState(context.self) + step3() + } + + def step3() = messageOrTimeout("step3") { case DurableStateCounter.State(1) => + whenDone ! "Durable State works" + Behaviors.stopped + } + + step1() + } + } + +} diff --git a/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala b/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala index 35658355..59bab439 100644 --- a/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala +++ b/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala @@ -12,7 +12,6 @@ import akka.persistence.typed.scaladsl.Effect import akka.persistence.typed.scaladsl.EventSourcedBehavior import akka.serialization.jackson.JsonSerializable import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty import scala.concurrent.duration.DurationInt @@ -20,16 +19,14 @@ object EventSourcedCounter { sealed trait Command extends JsonSerializable final case class Increase(amount: Int, replyTo: ActorRef[StatusReply[Increased]]) extends Command - @JsonCreator - final case class GetValue(replyTo: ActorRef[StatusReply[GetValueResponse]]) extends Command + final case class GetValue @JsonCreator() (replyTo: ActorRef[StatusReply[GetValueResponse]]) extends Command final case class GetValueResponse(value: Int) sealed trait Event extends JsonSerializable - // FIXME why doesn't @JsonCreator work as usual? is it something missing from the akka jackson feature? - final case class Increased(@JsonProperty("amount") amount: Int) extends Event + final case class Increased @JsonCreator() (amount: Int) extends Event - final case class State(@JsonProperty("value") value: Int) extends JsonSerializable + final case class State @JsonCreator() (value: Int) extends JsonSerializable def apply(id: String): Behavior[Command] = EventSourcedBehavior[Command, Event, State]( PersistenceId("EventSourcedHelloWorld", id), diff --git a/native-image-tests/src/main/scala/com/lightbend/Main.scala b/native-image-tests/src/main/scala/com/lightbend/Main.scala index 767439d7..6ceeeaeb 100644 --- a/native-image-tests/src/main/scala/com/lightbend/Main.scala +++ b/native-image-tests/src/main/scala/com/lightbend/Main.scala @@ -4,24 +4,36 @@ import akka.actor.typed.ActorSystem import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.Behaviors +import scala.concurrent.duration.DurationInt + object RootBehavior { def apply(): Behavior[AnyRef] = Behaviors.setup { context => - context.spawn(EsbTester(context.self), "ESBTester") + Behaviors.withTimers { timers => + timers.startSingleTimer("Timeout", 30.seconds) + context.spawn(EsbTester(context.self), "ESBTester") + context.spawn(DurableStateTester(context.self), "DurableStateTester") + + var awaitedOks = Set("ESB works", "Durable State works") - var awaitedOks = Set("ESB works") + Behaviors.receiveMessage { + case "Timeout" => + context.log.error("Suite of checks timed out, missing awaitedOks: {}", awaitedOks) + System.exit(1) + Behaviors.same - Behaviors.receiveMessage { - case string: String => - awaitedOks -= string - if (awaitedOks.isEmpty) { - context.log.info("All checks ok, shutting down") - Behaviors.stopped - } else { + case string: String => + awaitedOks -= string + if (awaitedOks.isEmpty) { + context.log.info("All checks ok, shutting down") + Behaviors.stopped + } else { + context.log.info("Continuing, awaitedOks not empty: {}", awaitedOks) + Behaviors.same + } + case other => + context.log.warn("Unexpected message: {}", other) Behaviors.same - } - case other => - context.log.warn("Unexpected message: {}", other) - Behaviors.same + } } } } From 8523b0d460685463fa114558b90431cbdd17d885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Fri, 23 Feb 2024 17:40:50 +0100 Subject: [PATCH 3/6] Test coverage for H2, H2-in-file and Postgres added, rudimentary doc page --- .github/workflows/native-image-tests.yml | 20 ++++++++++++++++++- docs/src/main/paradox/index.md | 1 + docs/src/main/paradox/native-image.md | 11 ++++++++++ .../main/resources/application-h2-file.conf | 9 +++++++++ .../main/resources/application-postgres.conf | 5 +++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/paradox/native-image.md create mode 100644 native-image-tests/src/main/resources/application-h2-file.conf create mode 100644 native-image-tests/src/main/resources/application-postgres.conf diff --git a/.github/workflows/native-image-tests.yml b/.github/workflows/native-image-tests.yml index bf1588cb..c9f22b75 100644 --- a/.github/workflows/native-image-tests.yml +++ b/.github/workflows/native-image-tests.yml @@ -51,9 +51,27 @@ jobs: run: |- cd native-image-tests/ sbt nativeImage -Dakka.r2dbc.version=`cat ~/.version` - # run the binary + + - name: Akka Persistence native image H2 inmem + run: |- + cd native-image-tests/ target/native-image/native-image-tests + - name: Akka Persistence native image H2 file + run: |- + cd native-image-tests/ + target/native-image/native-image-tests -Dconfig.resource=application-h2-file.conf + + - name: Start Postgres DB + run: |- + docker compose -f docker/docker-compose-postgres.yml up --wait + docker exec -i postgres-db psql -U postgres -t < ddl-scripts/create_tables_postgres.sql + + - name: Akka Persistence native image H2 + run: |- + cd native-image-tests/ + target/native-image/native-image-tests -Dconfig.resource=application-postgres.conf + - name: Email on failure if: ${{ failure() }} uses: dawidd6/action-send-mail@6063705cefe50cb915fc53bb06d4049cae2953b2 diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 76aa2128..a5f52b1c 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -19,6 +19,7 @@ The Akka Persistence R2DBC plugin allows for using SQL database with R2DBC as a * [Cleanup tool](cleanup.md) * [Migration tool](migration.md) * [Migration Guide](migration-guide.md) +* [Native Image](native-image.md) * [Contributing](contributing.md) @@@ diff --git a/docs/src/main/paradox/native-image.md b/docs/src/main/paradox/native-image.md new file mode 100644 index 00000000..b5ea31e6 --- /dev/null +++ b/docs/src/main/paradox/native-image.md @@ -0,0 +1,11 @@ +# Building Native Images + +Building native images with Akka HTTP is supported out of the box for the event sourced journal, snapshot store and +durable state store and databases: + +* H2 (inmem and file) +* Postgres + +Other databases can likely be used but will require figuring out and adding additional native-image metadata. + +For details about building native images with Akka in general, see the @extref[Akka Documentation](akka:additional/native-image.html). \ No newline at end of file diff --git a/native-image-tests/src/main/resources/application-h2-file.conf b/native-image-tests/src/main/resources/application-h2-file.conf new file mode 100644 index 00000000..3d7bd0c8 --- /dev/null +++ b/native-image-tests/src/main/resources/application-h2-file.conf @@ -0,0 +1,9 @@ +akka.persistence.journal.plugin = "akka.persistence.r2dbc.journal" +akka.persistence.snapshot-store.plugin = "akka.persistence.r2dbc.snapshot" +akka.persistence.state.plugin = "akka.persistence.r2dbc.state" + +akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.h2} +akka.persistence.r2dbc.connection-factory = { + protocol = "file" + database = "/tmp/test-h2-database" +} \ No newline at end of file diff --git a/native-image-tests/src/main/resources/application-postgres.conf b/native-image-tests/src/main/resources/application-postgres.conf new file mode 100644 index 00000000..9987ed55 --- /dev/null +++ b/native-image-tests/src/main/resources/application-postgres.conf @@ -0,0 +1,5 @@ +akka.persistence.journal.plugin = "akka.persistence.r2dbc.journal" +akka.persistence.snapshot-store.plugin = "akka.persistence.r2dbc.snapshot" +akka.persistence.state.plugin = "akka.persistence.r2dbc.state" + +akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.postgres} \ No newline at end of file From 3d35a28d3c15dd39d1f887593789d37aa167cd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Mon, 26 Feb 2024 14:11:19 +0100 Subject: [PATCH 4/6] minimal config, from latest Akka changes --- native-image-tests/build.sbt | 6 +- .../resources/META-INF/native-image/README.md | 1 - .../logback-classic/reflect-config.json | 65 ------------------- .../native-image.properties | 1 - .../native-image-tests/resource-config.json | 8 --- .../com/lightbend/DurableStateTester.scala | 8 +-- .../main/scala/com/lightbend/EsbTester.scala | 7 +- 7 files changed, 11 insertions(+), 85 deletions(-) delete mode 100644 native-image-tests/src/main/resources/META-INF/native-image/README.md delete mode 100644 native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json delete mode 100644 native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties delete mode 100644 native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json diff --git a/native-image-tests/build.sbt b/native-image-tests/build.sbt index 58f1ff8b..39026e3e 100644 --- a/native-image-tests/build.sbt +++ b/native-image-tests/build.sbt @@ -19,7 +19,11 @@ fork := true enablePlugins(NativeImagePlugin) nativeImageJvm := "graalvm-community" nativeImageVersion := "21.0.2" -nativeImageOptions := Seq("--no-fallback", "--verbose", "-Dakka.native-image.debug=true") +nativeImageOptions := Seq( + "--no-fallback", + "--verbose", + "--initialize-at-build-time=ch.qos.logback", + "-Dakka.native-image.debug=true") libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, diff --git a/native-image-tests/src/main/resources/META-INF/native-image/README.md b/native-image-tests/src/main/resources/META-INF/native-image/README.md deleted file mode 100644 index 6a176a67..00000000 --- a/native-image-tests/src/main/resources/META-INF/native-image/README.md +++ /dev/null @@ -1 +0,0 @@ -Custom config here is for logging using logback, loading keystore from classpath for TLS tests diff --git a/native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json b/native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json deleted file mode 100644 index 1b630ce7..00000000 --- a/native-image-tests/src/main/resources/META-INF/native-image/ch.qos.logback/logback-classic/reflect-config.json +++ /dev/null @@ -1,65 +0,0 @@ -[ - { - "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", - "queryAllPublicMethods":true, - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.DateConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.LevelConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.LineSeparatorConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.LoggerConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.MDCConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.MarkerConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.MessageConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.classic.pattern.ThreadConverter", - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.core.ConsoleAppender", - "queryAllPublicMethods":true, - "methods":[{"name":"","parameterTypes":[] }] - }, - { - "name":"ch.qos.logback.core.OutputStreamAppender", - "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] - }, - { - "name":"ch.qos.logback.core.encoder.Encoder", - "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] - }, - { - "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", - "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] - }, - { - "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", - "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] - }, - { - "name":"ch.qos.logback.core.spi.ContextAware", - "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] - } - -] diff --git a/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties b/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties deleted file mode 100644 index 3dea9206..00000000 --- a/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --initialize-at-build-time=org.slf4j.LoggerFactory,ch.qos.logback,org.slf4j.impl.StaticLoggerBinder,org.slf4j.MDC diff --git a/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json b/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json deleted file mode 100644 index e70119bc..00000000 --- a/native-image-tests/src/main/resources/META-INF/native-image/com.lightbend/native-image-tests/resource-config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "resources":{ - "includes":[{ - "pattern":"\\Qlogback.xml\\E" - }] - }, - "bundles":[] -} diff --git a/native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala b/native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala index 6017f71a..b295d376 100644 --- a/native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala +++ b/native-image-tests/src/main/scala/com/lightbend/DurableStateTester.scala @@ -10,7 +10,6 @@ import akka.persistence.typed.PersistenceId import akka.persistence.typed.state.scaladsl.DurableStateBehavior import akka.persistence.typed.state.scaladsl.Effect import akka.serialization.jackson.JsonSerializable -import com.fasterxml.jackson.annotation.JsonCreator import scala.concurrent.duration.DurationInt @@ -18,12 +17,11 @@ object DurableStateCounter { sealed trait Command extends JsonSerializable final case class Increase(amount: Int, replyTo: ActorRef[Increased]) extends Command - // FIXME why doesn't @JsonCreator work as usual? is it something missing from the jackson feature? - final case class GetState @JsonCreator() (replyTo: ActorRef[State]) extends Command + final case class GetState(replyTo: ActorRef[State]) extends Command - final case class Increased @JsonCreator() (newValue: Int) extends JsonSerializable + final case class Increased(newValue: Int) extends JsonSerializable - final case class State @JsonCreator() (value: Int) extends JsonSerializable + final case class State(value: Int) extends JsonSerializable def apply(id: String): Behavior[Command] = DurableStateBehavior[Command, State]( PersistenceId("DSCounter", id), diff --git a/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala b/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala index 59bab439..37d3acfc 100644 --- a/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala +++ b/native-image-tests/src/main/scala/com/lightbend/EsbTester.scala @@ -11,7 +11,6 @@ import akka.persistence.typed.PersistenceId import akka.persistence.typed.scaladsl.Effect import akka.persistence.typed.scaladsl.EventSourcedBehavior import akka.serialization.jackson.JsonSerializable -import com.fasterxml.jackson.annotation.JsonCreator import scala.concurrent.duration.DurationInt @@ -19,14 +18,14 @@ object EventSourcedCounter { sealed trait Command extends JsonSerializable final case class Increase(amount: Int, replyTo: ActorRef[StatusReply[Increased]]) extends Command - final case class GetValue @JsonCreator() (replyTo: ActorRef[StatusReply[GetValueResponse]]) extends Command + final case class GetValue(replyTo: ActorRef[StatusReply[GetValueResponse]]) extends Command final case class GetValueResponse(value: Int) sealed trait Event extends JsonSerializable - final case class Increased @JsonCreator() (amount: Int) extends Event + final case class Increased(amount: Int) extends Event - final case class State @JsonCreator() (value: Int) extends JsonSerializable + final case class State(value: Int) extends JsonSerializable def apply(id: String): Behavior[Command] = EventSourcedBehavior[Command, Event, State]( PersistenceId("EventSourcedHelloWorld", id), From 6248835078a444db538ad4b3dda4f253ef70644a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Tue, 27 Feb 2024 17:29:54 +0100 Subject: [PATCH 5/6] bump Akka version to the one with native-image metadata --- native-image-tests/build.sbt | 8 ++------ project/Dependencies.scala | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/native-image-tests/build.sbt b/native-image-tests/build.sbt index 39026e3e..944a5df3 100644 --- a/native-image-tests/build.sbt +++ b/native-image-tests/build.sbt @@ -6,13 +6,9 @@ scalaVersion := "2.13.12" resolvers += "Akka library repository".at("https://repo.akka.io/maven") -lazy val akkaVersion = sys.props.getOrElse("akka.version", "2.9.1") -lazy val akkaR2dbcVersion = sys.props.getOrElse("akka.r2dbc.version", "1.2.2") +lazy val akkaVersion = sys.props.getOrElse("akka.version", "2.9.2") +lazy val akkaR2dbcVersion = sys.props.getOrElse("akka.r2dbc.version", "1.2.3") -// Run in a separate JVM, to make sure sbt waits until all threads have -// finished before returning. -// If you want to keep the application running while executing other -// sbt tasks, consider https://github.com/spray/sbt-revolver/ fork := true // GraalVM native image build diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7fcdc2ab..c8ba0ca1 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,7 +9,7 @@ object Dependencies { val Scala3 = "3.3.1" val Scala2Versions = Seq(Scala213) val ScalaVersions = Dependencies.Scala2Versions :+ Dependencies.Scala3 - val AkkaVersion = System.getProperty("override.akka.version", "2.9.1") + val AkkaVersion = System.getProperty("override.akka.version", "2.9.2") val AkkaVersionInDocs = AkkaVersion.take(3) val AkkaPersistenceJdbcVersion = "5.2.0" // only in migration tool tests val AkkaProjectionVersionInDocs = "current" From e4821456ebf2a1ab4afa001cce56e559e1ce8fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Tue, 27 Feb 2024 17:31:13 +0100 Subject: [PATCH 6/6] review feedback --- docs/src/main/paradox/native-image.md | 4 ++-- .../src/main/resources/application-h2-file.conf | 2 +- .../src/main/resources/application-postgres.conf | 2 +- native-image-tests/src/main/resources/application.conf | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/main/paradox/native-image.md b/docs/src/main/paradox/native-image.md index b5ea31e6..d5dce223 100644 --- a/docs/src/main/paradox/native-image.md +++ b/docs/src/main/paradox/native-image.md @@ -1,7 +1,7 @@ # Building Native Images -Building native images with Akka HTTP is supported out of the box for the event sourced journal, snapshot store and -durable state store and databases: +Building native images with Akka Persistence R2DBC is supported out of the box for the event sourced journal, snapshot store and +durable state store with the following databases: * H2 (inmem and file) * Postgres diff --git a/native-image-tests/src/main/resources/application-h2-file.conf b/native-image-tests/src/main/resources/application-h2-file.conf index 3d7bd0c8..0c68654a 100644 --- a/native-image-tests/src/main/resources/application-h2-file.conf +++ b/native-image-tests/src/main/resources/application-h2-file.conf @@ -6,4 +6,4 @@ akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.h2} akka.persistence.r2dbc.connection-factory = { protocol = "file" database = "/tmp/test-h2-database" -} \ No newline at end of file +} diff --git a/native-image-tests/src/main/resources/application-postgres.conf b/native-image-tests/src/main/resources/application-postgres.conf index 9987ed55..6ae182fe 100644 --- a/native-image-tests/src/main/resources/application-postgres.conf +++ b/native-image-tests/src/main/resources/application-postgres.conf @@ -2,4 +2,4 @@ akka.persistence.journal.plugin = "akka.persistence.r2dbc.journal" akka.persistence.snapshot-store.plugin = "akka.persistence.r2dbc.snapshot" akka.persistence.state.plugin = "akka.persistence.r2dbc.state" -akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.postgres} \ No newline at end of file +akka.persistence.r2dbc.connection-factory = ${akka.persistence.r2dbc.postgres} diff --git a/native-image-tests/src/main/resources/application.conf b/native-image-tests/src/main/resources/application.conf index c20941bf..8e21e2ad 100644 --- a/native-image-tests/src/main/resources/application.conf +++ b/native-image-tests/src/main/resources/application.conf @@ -7,4 +7,4 @@ akka.persistence.r2dbc.connection-factory = { # overrides for default values from the 'akka.persistence.r2dbc.h2' config block protocol = "mem" database = "mydb" -} \ No newline at end of file +}