From 4180df2352a794812c6e1b3b555a0ef440dc2e8c Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Wed, 5 Feb 2025 00:02:17 +0800 Subject: [PATCH] added healthcheck command to client --- .../gbcs/cli/GradleBuildCacheServerCli.kt | 2 + .../cli/impl/commands/BenchmarkCommand.kt | 149 +++++++++--------- .../cli/impl/commands/HealthCheckCommand.kt | 45 ++++++ .../kotlin/net/woggioni/gbcs/client/Client.kt | 19 +++ 4 files changed, 141 insertions(+), 74 deletions(-) create mode 100644 gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/HealthCheckCommand.kt diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt index aa0afad..18aa30c 100644 --- a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt @@ -5,6 +5,7 @@ import net.woggioni.gbcs.cli.impl.GbcsCommand import net.woggioni.gbcs.cli.impl.commands.BenchmarkCommand import net.woggioni.gbcs.cli.impl.commands.ClientCommand import net.woggioni.gbcs.cli.impl.commands.GetCommand +import net.woggioni.gbcs.cli.impl.commands.HealthCheckCommand import net.woggioni.gbcs.cli.impl.commands.PasswordHashCommand import net.woggioni.gbcs.cli.impl.commands.PutCommand import net.woggioni.gbcs.cli.impl.commands.ServerCommand @@ -44,6 +45,7 @@ class GradleBuildCacheServerCli : GbcsCommand() { addSubcommand(BenchmarkCommand()) addSubcommand(PutCommand()) addSubcommand(GetCommand()) + addSubcommand(HealthCheckCommand()) }) System.exit(commandLine.execute(*args)) } diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/BenchmarkCommand.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/BenchmarkCommand.kt index 535d76d..c898414 100644 --- a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/BenchmarkCommand.kt +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/BenchmarkCommand.kt @@ -46,90 +46,91 @@ class BenchmarkCommand : GbcsCommand() { clientCommand.configuration.profiles[profileName] ?: throw IllegalArgumentException("Profile $profileName does not exist in configuration") } - val client = GradleBuildCacheClient(profile) + GradleBuildCacheClient(profile).use { client -> - val entryGenerator = sequence { - val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong()) - while (true) { - val key = JWO.bytesToHex(random.nextBytes(16)) - val content = random.nextInt().toByte() - val value = ByteArray(size, { _ -> content }) - yield(key to value) + val entryGenerator = sequence { + val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong()) + while (true) { + val key = JWO.bytesToHex(random.nextBytes(16)) + val content = random.nextInt().toByte() + val value = ByteArray(size, { _ -> content }) + yield(key to value) + } } - } - log.info { - "Starting insertion" - } - val entries = let { - val completionCounter = AtomicLong(0) - val completionQueue = LinkedBlockingQueue>(numberOfEntries) - val start = Instant.now() - val semaphore = Semaphore(profile.maxConnections * 3) - val iterator = entryGenerator.take(numberOfEntries).iterator() - while(completionCounter.get() < numberOfEntries) { - if(iterator.hasNext()) { - val entry = iterator.next() + log.info { + "Starting insertion" + } + val entries = let { + val completionCounter = AtomicLong(0) + val completionQueue = LinkedBlockingQueue>(numberOfEntries) + val start = Instant.now() + val semaphore = Semaphore(profile.maxConnections * 3) + val iterator = entryGenerator.take(numberOfEntries).iterator() + while (completionCounter.get() < numberOfEntries) { + if (iterator.hasNext()) { + val entry = iterator.next() + semaphore.acquire() + val future = client.put(entry.first, entry.second).thenApply { entry } + future.whenComplete { result, ex -> + if (ex != null) { + log.error(ex.message, ex) + } else { + completionQueue.put(result) + } + semaphore.release() + completionCounter.incrementAndGet() + } + } + } + + val inserted = completionQueue.toList() + val end = Instant.now() + log.info { + val elapsed = Duration.between(start, end).toMillis() + val opsPerSecond = String.format("%.2f", numberOfEntries.toDouble() / elapsed * 1000) + "Insertion rate: $opsPerSecond ops/s" + } + inserted + } + log.info { + "Inserted ${entries.size} entries" + } + log.info { + "Starting retrieval" + } + if (entries.isNotEmpty()) { + val completionCounter = AtomicLong(0) + val semaphore = Semaphore(profile.maxConnections * 3) + val start = Instant.now() + entries.forEach { entry -> semaphore.acquire() - val future = client.put(entry.first, entry.second).thenApply { entry } - future.whenComplete { result, ex -> - if (ex != null) { - log.error(ex.message, ex) - } else { - completionQueue.put(result) + + val future = client.get(entry.first).thenApply { + if (it == null) { + log.error { + "Missing entry for key '${entry.first}'" + } + } else if (!entry.second.contentEquals(it)) { + log.error { + "Retrieved a value different from what was inserted for key '${entry.first}'" + } } - semaphore.release() + } + future.whenComplete { _, _ -> completionCounter.incrementAndGet() + semaphore.release() } } - } - - val inserted = completionQueue.toList() - val end = Instant.now() - log.info { - val elapsed = Duration.between(start, end).toMillis() - val opsPerSecond = String.format("%.2f", numberOfEntries.toDouble() / elapsed * 1000) - "Insertion rate: $opsPerSecond ops/s" - } - inserted - } - log.info { - "Inserted ${entries.size} entries" - } - log.info { - "Starting retrieval" - } - if (entries.isNotEmpty()) { - val completionCounter = AtomicLong(0) - val semaphore = Semaphore(profile.maxConnections * 3) - val start = Instant.now() - entries.forEach { entry -> - semaphore.acquire() - - val future = client.get(entry.first).thenApply { - if (it == null) { - log.error { - "Missing entry for key '${entry.first}'" - } - } else if (!entry.second.contentEquals(it)) { - log.error { - "Retrieved a value different from what was inserted for key '${entry.first}'" - } - } - } - future.whenComplete { _, _ -> - completionCounter.incrementAndGet() - semaphore.release() + val end = Instant.now() + log.info { + val elapsed = Duration.between(start, end).toMillis() + val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000) + "Retrieval rate: $opsPerSecond ops/s" } + } else { + log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache") } - val end = Instant.now() - log.info { - val elapsed = Duration.between(start, end).toMillis() - val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000) - "Retrieval rate: $opsPerSecond ops/s" - } - } else { - log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache") } } } \ No newline at end of file diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/HealthCheckCommand.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/HealthCheckCommand.kt new file mode 100644 index 0000000..0440f52 --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/HealthCheckCommand.kt @@ -0,0 +1,45 @@ +package net.woggioni.gbcs.cli.impl.commands + +import net.woggioni.gbcs.cli.impl.GbcsCommand +import net.woggioni.gbcs.client.GradleBuildCacheClient +import net.woggioni.gbcs.common.contextLogger +import picocli.CommandLine +import java.security.SecureRandom +import kotlin.random.Random + +@CommandLine.Command( + name = "health", + description = ["Check server health"], + showDefaultValues = true +) +class HealthCheckCommand : GbcsCommand() { + private val log = contextLogger() + + @CommandLine.Spec + private lateinit var spec: CommandLine.Model.CommandSpec + + override fun run() { + val clientCommand = spec.parent().userObject() as ClientCommand + val profile = clientCommand.profileName.let { profileName -> + clientCommand.configuration.profiles[profileName] + ?: throw IllegalArgumentException("Profile $profileName does not exist in configuration") + } + GradleBuildCacheClient(profile).use { client -> + val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong()) + val nonce = ByteArray(0xa0) + random.nextBytes(nonce) + client.healthCheck(nonce).thenApply { value -> + if(value == null) { + throw IllegalStateException("Empty response from server") + } + for(i in 0 until nonce.size) { + for(j in value.size - nonce.size until nonce.size) { + if(nonce[i] != value[j]) { + throw IllegalStateException("Server nonce does not match") + } + } + } + }.get() + } + } +} \ No newline at end of file diff --git a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Client.kt b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Client.kt index 05eb5a4..09a5165 100644 --- a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Client.kt +++ b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Client.kt @@ -213,6 +213,25 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC } } + fun healthCheck(nonce: ByteArray): CompletableFuture { + return executeWithRetry { + sendRequest(profile.serverURI, HttpMethod.TRACE, nonce) + }.thenApply { + val status = it.status() + if (it.status() != HttpResponseStatus.OK) { + throw HttpException(status) + } else { + it.content() + } + }.thenApply { maybeByteBuf -> + maybeByteBuf?.let { + val result = ByteArray(it.readableBytes()) + it.getBytes(0, result) + result + } + } + } + fun get(key: String): CompletableFuture { return executeWithRetry { sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)