Skip to content

Commit 3cd2d36

Browse files
authored
feat: add metrics to sleeping barber (#32)
1 parent bd9267b commit 3cd2d36

File tree

13 files changed

+338
-167
lines changed

13 files changed

+338
-167
lines changed

.github/workflows/docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
target: [ diningPhilosophers, count, cigaretteSmokers, chameneos, big ]
18+
target: [ diningPhilosophers, count, cigaretteSmokers, chameneos, big, sleepingBarber ]
1919
permissions:
2020
packages: write
2121
contents: read

sleepingBarber.Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM gradle:9.3.0-jdk25 AS build
2+
3+
COPY --chown=gradle:gradle . /usr/src/cirrina-baselines
4+
5+
WORKDIR /usr/src/cirrina-baselines
6+
7+
RUN gradle :sleepingBarber:distZip
8+
9+
RUN unzip sleepingBarber/build/distributions/sleepingBarber.zip -d /tmp
10+
11+
FROM gcr.io/distroless/java25-debian13 AS runtime
12+
13+
COPY --from=build /tmp/sleepingBarber /opt/sleepingBarber
14+
15+
ENTRYPOINT ["java", "-cp", "/opt/sleepingBarber/lib/*", "ac.at.uibk.dps.dapr.barber.SleepingBarberKt"]

sleepingBarber.Vagrantfile

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
2+
Vagrant.configure("2") do |config|
3+
nodes = {
4+
"redis_pub_sub" => {},
5+
"waiting_room" => {},
6+
"barber" => {},
7+
"w0" => { "CUSTOMER_ID" => "0" },
8+
"w1" => { "CUSTOMER_ID" => "1" },
9+
"w2" => { "CUSTOMER_ID" => "2" },
10+
"w3" => { "CUSTOMER_ID" => "3" },
11+
"w4" => { "CUSTOMER_ID" => "4" }
12+
## Add more Workers for final Benchmark
13+
}
14+
15+
nodes.each do |name, env_vars|
16+
config.vm.provider "virtualbox" do |vb|
17+
vb.customize ["guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-interval", 10000]
18+
vb.customize ["guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 0.2]
19+
end
20+
config.vm.define name do |node|
21+
node.vm.box = "generic/ubuntu2004"
22+
ip_suffix = case name
23+
when "waiting_room" then 10
24+
when "barber" then 9
25+
when "redis_pub_sub" then 8
26+
else 11 + name[1..-1].to_i
27+
end
28+
node.vm.network "private_network", ip: "192.168.56.#{ip_suffix}"
29+
node.vm.synced_folder ".", "/app"
30+
node.vm.provision "shell", inline: <<-SHELL
31+
apt-get update -qq && apt-get install -y docker.io linuxptp
32+
33+
# Every node gets own state store
34+
docker rm -f redis || true
35+
docker run -d --name redis --network host redis:8.2.4-alpine
36+
37+
if [ "#{name}" = "redis_pub_sub" ]; then
38+
exit 0
39+
fi
40+
41+
# Every node gets its own placement
42+
docker rm -f placement || true
43+
docker run -d --name placement --network host daprio/placement:1.16.0 ./placement --port 50006
44+
45+
# Sidecar
46+
docker rm -f #{name}-sidecar || true
47+
docker run -d \
48+
--name #{name}-sidecar \
49+
--network host \
50+
-v /app/config/redis/components-vagrant:/components \
51+
daprio/daprd:edge \
52+
./daprd \
53+
--app-id #{name}-sidecar \
54+
--app-port 3000 \
55+
--resources-path /components \
56+
--placement-host-address localhost:50006 \
57+
--metrics-port 9091
58+
59+
sleep 2
60+
61+
# Application
62+
docker rm -f #{name} || true
63+
if [ "#{name}" = "waiting_room" ]; then
64+
docker run -d \
65+
--name #{name} \
66+
--network host \
67+
-e ROLE=waiting_room \
68+
-e ROOM_SLOTS=5 \
69+
-e NUMBER_OF_PHILOSOPHERS=6 \
70+
-e DAPR_HTTP_ENDPOINT=http://localhost:3500 \
71+
-e METRICS_DIRECTORY=/app/metrics \
72+
-e DAPR_GRPC_ENDPOINT=http://localhost:50001 \
73+
-v /app/sleepingBarber/run/#{name}/metrics:/app/metrics \
74+
collaborativestatemachines/cirrina-baselines-sleepingBarber:unstable
75+
elif [ "#{name}" = "barber" ]; then
76+
docker run -d \
77+
--name #{name} \
78+
--network host \
79+
-e ROLE=barber \
80+
-e DAPR_HTTP_ENDPOINT=http://localhost:3500 \
81+
-e METRICS_DIRECTORY=/app/metrics \
82+
-e DAPR_GRPC_ENDPOINT=http://localhost:50001 \
83+
-v /app/sleepingBarber/run/#{name}/metrics:/app/metrics \
84+
collaborativestatemachines/cirrina-baselines-sleepingBarber:unstable
85+
else
86+
docker run -d \
87+
--name #{name} \
88+
--network host \
89+
-e CUSTOMER_ID=#{env_vars['CUSTOMER_ID']} \
90+
-e DAPR_HTTP_ENDPOINT=http://localhost:3500 \
91+
-e METRICS_DIRECTORY=/app/metrics \
92+
-e DAPR_GRPC_ENDPOINT=http://localhost:50001 \
93+
-v /app/sleepingBarber/run/#{name}/metrics:/app/metrics \
94+
collaborativestatemachines/cirrina-baselines-sleepingBarber:unstable
95+
fi
96+
97+
nohup bash -c 'while true; do sudo docker stats --no-stream --format "$(date +%s),{{.Name}},{{.CPUPerc}},{{.MemUsage}}" >> /app/sleepingBarber/run/metrics_#{name}/docker_stats.csv; sleep 1; done' &
98+
SHELL
99+
end
100+
end
101+
end
Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
package ac.at.uibk.dps.dapr.barber
22

3+
import ac.at.uibk.dps.dapr.barber.barber.BarberActor
34
import ac.at.uibk.dps.dapr.barber.barber.BarberActorImpl
45
import ac.at.uibk.dps.dapr.barber.customer.CustomerActor
56
import ac.at.uibk.dps.dapr.barber.customer.CustomerActorImpl
67
import ac.at.uibk.dps.dapr.barber.waitingroom.WaitingRoomActorImpl
8+
import com.codahale.metrics.CsvReporter
9+
import com.codahale.metrics.MetricRegistry
710
import io.dapr.actors.ActorId
811
import io.dapr.actors.client.ActorClient
912
import io.dapr.actors.client.ActorProxyBuilder
1013
import io.dapr.actors.runtime.ActorRuntime
11-
import io.dapr.client.DaprClient
12-
import io.dapr.client.DaprClientBuilder
13-
import org.slf4j.Logger
14-
import org.slf4j.LoggerFactory
14+
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics
15+
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics
16+
import io.micrometer.core.instrument.binder.system.ProcessorMetrics
17+
import io.micrometer.core.instrument.dropwizard.DropwizardConfig
18+
import io.micrometer.core.instrument.dropwizard.DropwizardMeterRegistry
19+
import io.micrometer.core.instrument.util.HierarchicalNameMapper
20+
import java.nio.file.Files
21+
import java.nio.file.Paths
22+
import java.util.concurrent.TimeUnit
23+
import kotlin.apply
1524
import org.springframework.boot.ApplicationArguments
1625
import org.springframework.boot.ApplicationRunner
1726
import org.springframework.boot.autoconfigure.SpringBootApplication
@@ -21,9 +30,32 @@ import org.springframework.stereotype.Component
2130
@SpringBootApplication
2231
class SleepingBarber {
2332
companion object {
24-
val logger: Logger = LoggerFactory.getLogger(SleepingBarber::class.java)
25-
val daprClient: DaprClient = DaprClientBuilder().build()
26-
val actorClient: ActorClient = ActorClient()
33+
val metricsDirectory = System.getenv("METRICS_DIRECTORY") ?: "metrics"
34+
val metricsPeriod = System.getenv("METRICS_PERIOD")?.toLong() ?: 1L
35+
val metricsRegistry =
36+
MetricRegistry().apply {
37+
val path = Paths.get(metricsDirectory).toAbsolutePath()
38+
Files.createDirectories(path)
39+
CsvReporter.forRegistry(this).build(path.toFile()).start(metricsPeriod, TimeUnit.SECONDS)
40+
object :
41+
DropwizardMeterRegistry(
42+
object : DropwizardConfig {
43+
override fun get(key: String): String? = null
44+
45+
override fun prefix(): String = ""
46+
},
47+
this,
48+
HierarchicalNameMapper.DEFAULT,
49+
io.micrometer.core.instrument.Clock.SYSTEM,
50+
) {
51+
override fun nullGaugeValue(): Double = Double.NaN
52+
}
53+
.apply {
54+
ProcessorMetrics().bindTo(this)
55+
JvmMemoryMetrics().bindTo(this)
56+
JvmGcMetrics().bindTo(this)
57+
}
58+
}
2759
}
2860
}
2961

@@ -41,11 +73,13 @@ class AutoStarter : ApplicationRunner {
4173

4274
override fun run(args: ApplicationArguments?) {
4375
val role = System.getenv("ROLE") ?: "customer"
44-
if (role != "customer") return
45-
val id = System.getenv("CUSTOMER_ID")
46-
val proxy =
47-
ActorProxyBuilder(CustomerActor::class.java, SleepingBarber.actorClient).build(ActorId(id))
48-
proxy.enterWaitingRoom().subscribe()
49-
SleepingBarber.logger.info("customer $id started initial")
76+
ActorClient().use { client ->
77+
if (role == "customer") {
78+
val id = System.getenv("CUSTOMER_ID")
79+
ActorProxyBuilder(CustomerActor::class.java, client).build(ActorId(id)).request()
80+
} else if (role == "barber") {
81+
ActorProxyBuilder(BarberActor::class.java, client).build(ActorId("barber")).sleeping()
82+
}
83+
}
5084
}
5185
}

sleepingBarber/src/main/kotlin/ac/at/uibk/dps/dapr/barber/barber/BarberActor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package ac.at.uibk.dps.dapr.barber.barber
22

33
import io.dapr.actors.ActorMethod
44
import io.dapr.actors.ActorType
5-
import reactor.core.publisher.Mono
65

76
@ActorType(name = "BarberActor")
87
interface BarberActor {
8+
@ActorMethod(name = "sleeping") fun sleeping()
99

10-
@ActorMethod(name = "cuttingHair") fun cuttingHair(customerId: Int): Mono<Void>
10+
@ActorMethod(name = "cutting") fun cutting(data: Map<String, Any>)
1111
}
Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,67 @@
11
package ac.at.uibk.dps.dapr.barber.barber
22

3-
import ac.at.uibk.dps.dapr.barber.SleepingBarber
4-
import ac.at.uibk.dps.dapr.barber.customer.CustomerPubSub
5-
import ac.at.uibk.dps.dapr.barber.waitingroom.WaitingRoomPubSub
3+
import ac.at.uibk.dps.dapr.barber.SleepingBarber.Companion.metricsRegistry
64
import io.dapr.actors.ActorId
75
import io.dapr.actors.runtime.AbstractActor
86
import io.dapr.actors.runtime.ActorRuntimeContext
9-
import java.time.Duration
10-
import reactor.core.publisher.Mono
7+
import io.dapr.client.DaprClient
8+
import io.dapr.client.DaprClientBuilder
9+
import java.lang.Thread.sleep
10+
import java.security.SecureRandom
11+
import java.util.concurrent.TimeUnit
12+
import kotlin.random.Random
13+
import kotlin.time.Clock
1114

1215
class BarberActorImpl(runtimeContext: ActorRuntimeContext<BarberActorImpl>, id: ActorId) :
1316
AbstractActor(runtimeContext, id), BarberActor {
17+
val client: DaprClient? = DaprClientBuilder().build()
1418

15-
val cuttingTimeMS = System.getenv("CUTTING_TIME_MS")?.toInt() ?: 0
19+
private val seedGenerator = SecureRandom()
1620

17-
override fun cuttingHair(customerId: Int): Mono<Void> {
18-
return Mono.delay(Duration.ofMillis(cuttingTimeMS.toLong())).flatMap {
19-
Mono.`when`(
20-
WaitingRoomPubSub.barberFinished(SleepingBarber.daprClient),
21-
CustomerPubSub.done(SleepingBarber.daprClient, customerId),
22-
)
21+
private val threadRng =
22+
object : ThreadLocal<Random>() {
23+
override fun initialValue(): Random {
24+
return Random(seedGenerator.nextLong())
25+
}
2326
}
27+
28+
override fun sleeping() {
29+
client!!.publishEvent("pubsub", "ready", getMap()).subscribe()
30+
}
31+
32+
override fun cutting(data: Map<String, Any>) {
33+
measureEventTime(data)
34+
val customer = data["id"].toString().toInt()
35+
client!!.publishEvent("pubsub", "comeIn", getMap(customer)).subscribe()
36+
sleep(randomAround(10, 2).toLong())
37+
client.publishEvent("pubsub", "done", getMap(customer)).subscribe()
38+
sleeping()
39+
}
40+
41+
fun randomAround(base: Int, delta: Int): Int {
42+
return (base - delta..base + delta).random(threadRng.get())
43+
}
44+
45+
private fun getMap(): Map<String, Any> {
46+
val now = Clock.System.now()
47+
val epochNanos = (now.epochSeconds * 1_000_000_000L) + now.nanosecondsOfSecond
48+
49+
return mapOf("time" to epochNanos)
50+
}
51+
52+
private fun getMap(i: Int): Map<String, Any> {
53+
val now = Clock.System.now()
54+
val epochNanos = (now.epochSeconds * 1_000_000_000L) + now.nanosecondsOfSecond
55+
56+
return mapOf("id" to i, "time" to epochNanos)
57+
}
58+
59+
private fun measureEventTime(data: Map<String, Any>) {
60+
val now = Clock.System.now()
61+
val nowNanos = (now.epochSeconds * 1_000_000_000L) + now.nanosecondsOfSecond
62+
63+
val deltaNanos = (nowNanos - data["time"] as Long).coerceAtLeast(0L)
64+
65+
metricsRegistry.timer("event.latency")!!.update((deltaNanos), TimeUnit.NANOSECONDS)
2466
}
2567
}
Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,24 @@
11
package ac.at.uibk.dps.dapr.barber.barber
22

3-
import ac.at.uibk.dps.dapr.barber.SleepingBarber
43
import io.dapr.Topic
54
import io.dapr.actors.ActorId
5+
import io.dapr.actors.client.ActorClient
66
import io.dapr.actors.client.ActorProxyBuilder
7-
import io.dapr.client.DaprClient
87
import io.dapr.client.domain.CloudEvent
98
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
109
import org.springframework.web.bind.annotation.PostMapping
1110
import org.springframework.web.bind.annotation.RequestBody
1211
import org.springframework.web.bind.annotation.RestController
13-
import reactor.core.publisher.Mono
1412

1513
@RestController
1614
@ConditionalOnProperty("app.role", havingValue = "barber")
1715
class BarberPubSub {
18-
19-
companion object {
20-
const val PUB_SUB_NAME = "barber_pub_sub"
21-
const val CUTTING_TOPIC = "cutting"
22-
const val BARBER_NAME = "barber"
23-
24-
fun cutting(client: DaprClient, id: Int): Mono<Void> {
25-
return client.publishEvent(PUB_SUB_NAME, CUTTING_TOPIC, id)
26-
}
27-
}
28-
2916
val barberActor: BarberActor? =
30-
ActorProxyBuilder(BarberActor::class.java, SleepingBarber.actorClient)
31-
.build(ActorId(BARBER_NAME))
17+
ActorProxyBuilder(BarberActor::class.java, ActorClient()).build(ActorId("barber"))
3218

33-
@Topic(name = CUTTING_TOPIC, pubsubName = PUB_SUB_NAME)
34-
@PostMapping("/$CUTTING_TOPIC")
35-
fun cuttingSubscriber(@RequestBody event: CloudEvent<Int>) {
36-
barberActor!!.cuttingHair(event.data).subscribe()
19+
@Topic(name = "cutting", pubsubName = "pubsub")
20+
@PostMapping("/cutting")
21+
fun cuttingSubscriber(@RequestBody event: CloudEvent<Map<String, Any>>) {
22+
barberActor!!.cutting(event.data)
3723
}
3824
}

sleepingBarber/src/main/kotlin/ac/at/uibk/dps/dapr/barber/customer/CustomerActor.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package ac.at.uibk.dps.dapr.barber.customer
22

33
import io.dapr.actors.ActorMethod
44
import io.dapr.actors.ActorType
5-
import reactor.core.publisher.Mono
65

76
@ActorType(name = "CustomerActor")
87
interface CustomerActor {
8+
@ActorMethod(name = "request") fun request()
99

10-
@ActorMethod(name = "enterWaitingRoom") fun enterWaitingRoom(): Mono<Void>
10+
@ActorMethod(name = "full") fun full(data: Map<String, Any>)
1111

12-
@ActorMethod(name = "waitingRoomFull") fun waitingRoomFull(): Mono<Void>
12+
@ActorMethod(name = "comeIn") fun comeIn(data: Map<String, Any>)
1313

14-
@ActorMethod(name = "doneCutting") fun doneCutting(): Mono<Void>
14+
@ActorMethod(name = "done") fun done(data: Map<String, Any>)
1515
}

0 commit comments

Comments
 (0)