diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1a2b416..0c705f3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - target: [ diningPhilosophers, count, cigaretteSmokers, chameneos, big, sleepingBarber ] + target: [ diningPhilosophers, count, cigaretteSmokers, chameneos, big, sleepingBarber, pingPong ] permissions: packages: write contents: read @@ -54,4 +54,4 @@ jobs: file: ./${{ matrix.target }}.Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} diff --git a/pingPong.Dockerfile b/pingPong.Dockerfile new file mode 100644 index 0000000..bd98b0f --- /dev/null +++ b/pingPong.Dockerfile @@ -0,0 +1,15 @@ +FROM gradle:9.3.0-jdk25 AS build + +COPY --chown=gradle:gradle . /usr/src/cirrina-baselines + +WORKDIR /usr/src/cirrina-baselines + +RUN gradle :pingPong:distZip + +RUN unzip pingPong/build/distributions/pingPong.zip -d /tmp + +FROM gcr.io/distroless/java25-debian13 AS runtime + +COPY --from=build /tmp/pingPong /opt/pingPong + +ENTRYPOINT ["java", "-cp", "/opt/pingPong/lib/*", "ac.at.uibk.dps.dapr.pingPong.PingPongKt"] diff --git a/pingPong/build.gradle.kts b/pingPong/build.gradle.kts new file mode 100644 index 0000000..85c4c0a --- /dev/null +++ b/pingPong/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { id("common-conventions") } + +application { mainClass.set("ac.at.uibk.dps.dapr.pingPong.PingPongKt") } diff --git a/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/PingPong.kt b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/PingPong.kt new file mode 100644 index 0000000..982c43b --- /dev/null +++ b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/PingPong.kt @@ -0,0 +1,76 @@ +package ac.at.uibk.dps.dapr.pingPong + +import ac.at.uibk.dps.dapr.pingPong.ping.PingActor +import ac.at.uibk.dps.dapr.pingPong.ping.PingActorImpl +import ac.at.uibk.dps.dapr.pingPong.pong.PongActorImpl +import com.codahale.metrics.CsvReporter +import com.codahale.metrics.MetricRegistry +import io.dapr.actors.ActorId +import io.dapr.actors.client.ActorClient +import io.dapr.actors.client.ActorProxyBuilder +import io.dapr.actors.runtime.ActorRuntime +import io.micrometer.core.instrument.Clock +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics +import io.micrometer.core.instrument.binder.system.ProcessorMetrics +import io.micrometer.core.instrument.dropwizard.DropwizardConfig +import io.micrometer.core.instrument.dropwizard.DropwizardMeterRegistry +import io.micrometer.core.instrument.util.HierarchicalNameMapper +import java.nio.file.Paths +import java.util.concurrent.TimeUnit +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.stereotype.Component + +@SpringBootApplication +class PingPong { + companion object { + val metricsDirectory = System.getenv("METRICS_DIRECTORY") ?: "metrics" + + fun provideMetricRegistry(): MetricRegistry = + MetricRegistry().apply { + val path = Paths.get(metricsDirectory).toAbsolutePath() + + CsvReporter.forRegistry(this).build(path.toFile()).start(1L, TimeUnit.SECONDS) + + object : + DropwizardMeterRegistry( + object : DropwizardConfig { + override fun get(key: String): String? = null + + override fun prefix(): String = "" + }, + this, + HierarchicalNameMapper.DEFAULT, + Clock.SYSTEM, + ) { + override fun nullGaugeValue(): Double = Double.NaN + } + .apply { + ProcessorMetrics().bindTo(this) + JvmMemoryMetrics().bindTo(this) + JvmGcMetrics().bindTo(this) + } + } + } +} + +fun main(args: Array) { + val role = System.getenv("ROLE") + if (role == "ping") ActorRuntime.getInstance().registerActor(PingActorImpl::class.java) + if (role == "pong") ActorRuntime.getInstance().registerActor(PongActorImpl::class.java) + runApplication(*args) +} + +@Component +class AutoStarter : ApplicationRunner { + + override fun run(args: ApplicationArguments?) { + val role = System.getenv("ROLE") ?: "ping" + if (role != "ping") return + val proxy = ActorProxyBuilder(PingActor::class.java, ActorClient()).build(ActorId("ping-1")) + proxy.ping(-1L) + } +} diff --git a/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingActor.kt b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingActor.kt new file mode 100644 index 0000000..dcbc7df --- /dev/null +++ b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingActor.kt @@ -0,0 +1,8 @@ +package ac.at.uibk.dps.dapr.pingPong.ping + +import io.dapr.actors.ActorType + +@ActorType(name = "PingActor") +interface PingActor { + fun ping(time: Long) +} diff --git a/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingActorImpl.kt b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingActorImpl.kt new file mode 100644 index 0000000..34f5a0d --- /dev/null +++ b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingActorImpl.kt @@ -0,0 +1,29 @@ +package ac.at.uibk.dps.dapr.pingPong.ping + +import ac.at.uibk.dps.dapr.pingPong.PingPong +import io.dapr.actors.ActorId +import io.dapr.actors.runtime.AbstractActor +import io.dapr.actors.runtime.ActorRuntimeContext +import io.dapr.client.DaprClient +import io.dapr.client.DaprClientBuilder +import java.util.concurrent.TimeUnit +import kotlin.time.Clock + +class PingActorImpl(runtimeContext: ActorRuntimeContext, actorId: ActorId) : + AbstractActor(runtimeContext, actorId), PingActor { + val client: DaprClient = DaprClientBuilder().build() + + var metricRegistry = PingPong.provideMetricRegistry() + + override fun ping(time: Long) { + val now = Clock.System.now() + val nowNanos = (now.epochSeconds * 1_000_000_000L) + now.nanosecondsOfSecond + val deltaNanos = (nowNanos - time).coerceAtLeast(0L) + + if (time >= 0) metricRegistry.timer("event.latency").update((deltaNanos), TimeUnit.NANOSECONDS) + + client.publishEvent("pubsub", "ping", nowNanos).subscribe() + + metricRegistry.counter("ping.count").inc() + } +} diff --git a/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingSubscriber.kt b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingSubscriber.kt new file mode 100644 index 0000000..2140947 --- /dev/null +++ b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/ping/PingSubscriber.kt @@ -0,0 +1,24 @@ +package ac.at.uibk.dps.dapr.pingPong.ping + +import io.dapr.Topic +import io.dapr.actors.ActorId +import io.dapr.actors.client.ActorClient +import io.dapr.actors.client.ActorProxyBuilder +import io.dapr.client.domain.CloudEvent +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +@ConditionalOnProperty("app.role", havingValue = "ping") +class PingSubscriber { + private val pingProxy = + ActorProxyBuilder(PingActor::class.java, ActorClient()).build(ActorId("ping-1")) + + @Topic(name = "pong", pubsubName = "pubsub") + @PostMapping("/pong") + fun pongSubscriber(@RequestBody event: CloudEvent) { + pingProxy.ping(event.data) + } +} diff --git a/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongActor.kt b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongActor.kt new file mode 100644 index 0000000..145e6f1 --- /dev/null +++ b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongActor.kt @@ -0,0 +1,8 @@ +package ac.at.uibk.dps.dapr.pingPong.pong + +import io.dapr.actors.ActorType + +@ActorType(name = "PongActor") +interface PongActor { + fun pong(time: Long) +} diff --git a/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongActorImpl.kt b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongActorImpl.kt new file mode 100644 index 0000000..6401fe8 --- /dev/null +++ b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongActorImpl.kt @@ -0,0 +1,27 @@ +package ac.at.uibk.dps.dapr.pingPong.pong + +import ac.at.uibk.dps.dapr.pingPong.PingPong +import io.dapr.actors.ActorId +import io.dapr.actors.runtime.AbstractActor +import io.dapr.actors.runtime.ActorRuntimeContext +import io.dapr.client.DaprClient +import io.dapr.client.DaprClientBuilder +import java.util.concurrent.TimeUnit +import kotlin.time.Clock + +class PongActorImpl(runtimeContext: ActorRuntimeContext, actorId: ActorId) : + AbstractActor(runtimeContext, actorId), PongActor { + val client: DaprClient = DaprClientBuilder().build() + + var metricRegistry = PingPong.provideMetricRegistry() + + override fun pong(time: Long) { + val now = Clock.System.now() + val nowNanos = (now.epochSeconds * 1_000_000_000L) + now.nanosecondsOfSecond + val deltaNanos = (nowNanos - time).coerceAtLeast(0L) + + metricRegistry.timer("event.latency").update((deltaNanos), TimeUnit.NANOSECONDS) + + client.publishEvent("pubsub", "pong", nowNanos).subscribe() + } +} diff --git a/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongSubscriber.kt b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongSubscriber.kt new file mode 100644 index 0000000..04bde06 --- /dev/null +++ b/pingPong/src/main/kotlin/ac.at.uibk.dps.dapr.pingPong/pong/PongSubscriber.kt @@ -0,0 +1,24 @@ +package ac.at.uibk.dps.dapr.pingPong.pong + +import io.dapr.Topic +import io.dapr.actors.ActorId +import io.dapr.actors.client.ActorClient +import io.dapr.actors.client.ActorProxyBuilder +import io.dapr.client.domain.CloudEvent +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +@ConditionalOnProperty("app.role", havingValue = "pong") +class PongSubscriber { + private val pongProxy = + ActorProxyBuilder(PongActor::class.java, ActorClient()).build(ActorId("pong-1")) + + @Topic(name = "ping", pubsubName = "pubsub") + @PostMapping("/ping") + fun pingSubscriber(@RequestBody event: CloudEvent) { + pongProxy.pong(event.data) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 396d3d5..7209c8e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,3 +11,5 @@ include("count") include("diningPhilosophers") include("sleepingBarber") + +include("pingPong") \ No newline at end of file