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] 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") + +}