diff --git a/Dockerfile b/Dockerfile index 546e46c..51275c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /home/ubuntu RUN mkdir gbcs WORKDIR /home/ubuntu/gbcs -COPY --chown=ubuntu:users .git .git +COPY --chown=ubuntu:users ./.git ./.git COPY --chown=ubuntu:users gbcs-base gbcs-base COPY --chown=ubuntu:users gbcs-api gbcs-api COPY --chown=ubuntu:users gbcs-memcached gbcs-memcached @@ -21,7 +21,7 @@ COPY --chown=ubuntu:users gradle.properties gradle.properties COPY --chown=ubuntu:users gradle gradle COPY --chown=ubuntu:users gradlew gradlew -RUN --mount=type=cache,target=/home/ubuntu/.gradle,uid=1000,gid=1000 ./gradlew --no-daemon assemble +RUN --mount=type=cache,target=/home/ubuntu/.gradle,uid=1000,gid=1000 ./gradlew --no-daemon clean assemble FROM alpine:latest AS base-release RUN --mount=type=cache,target=/var/cache/apk apk update diff --git a/conf/gbcs-memcached.xml b/conf/gbcs-memcached.xml index 37015c2..10fe02b 100644 --- a/conf/gbcs-memcached.xml +++ b/conf/gbcs-memcached.xml @@ -1,5 +1,5 @@ - 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 753bf1c..5e8c932 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 @@ -92,7 +92,14 @@ class GradleBuildCacheServerCli(application : Application, private val log : Log "Server configuration:\n${String(it.toByteArray())}" } } - GradleBuildCacheServer(configuration).run().use { + val server = GradleBuildCacheServer(configuration) + server.run().use { + log.info { + "GradleBuildCacheServer is listening on ${configuration.host}:${configuration.port}" + } + } + log.info { + "GradleBuildCacheServer has been gracefully shut down" } } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f526aa9..6af0cd9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,6 @@ org.gradle.caching=true gbcs.version = 0.0.1 -lys.version = 2025.01.09 +lys.version = 2025.01.10 gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven diff --git a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt index 4f9eab9..f1c76ae 100644 --- a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt +++ b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt @@ -12,7 +12,6 @@ import io.netty.channel.ChannelInitializer import io.netty.channel.ChannelOption import io.netty.channel.ChannelPromise import io.netty.channel.DefaultFileRegion -import io.netty.channel.EventLoopGroup import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.nio.NioServerSocketChannel @@ -69,7 +68,6 @@ import java.security.PrivateKey import java.security.cert.X509Certificate import java.util.Arrays import java.util.Base64 -import java.util.concurrent.Executors import java.util.regex.Matcher import java.util.regex.Pattern import javax.naming.ldap.LdapName @@ -178,40 +176,39 @@ class GradleBuildCacheServer(private val cfg: Configuration) { } } - private class ServerInitializer(private val cfg: Configuration) : ChannelInitializer() { - - private fun createSslCtx(tls: Configuration.Tls): SslContext { - val keyStore = tls.keyStore - return if (keyStore == null) { - throw IllegalArgumentException("No keystore configured") - } else { - val javaKeyStore = loadKeystore(keyStore.file, keyStore.password) - val serverKey = javaKeyStore.getKey( - keyStore.keyAlias, keyStore.keyPassword?.let(String::toCharArray) - ) as PrivateKey - val serverCert: Array = - Arrays.stream(javaKeyStore.getCertificateChain(keyStore.keyAlias)) - .map { it as X509Certificate } - .toArray { size -> Array(size) { null } } - SslContextBuilder.forServer(serverKey, *serverCert).apply { - if (tls.isVerifyClients) { - clientAuth(ClientAuth.OPTIONAL) - val trustStore = tls.trustStore - if (trustStore != null) { - val ts = loadKeystore(trustStore.file, trustStore.password) - trustManager( - ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus) - ) - } - } - }.build() - } - } - - private val sslContext: SslContext? = cfg.tls?.let(this::createSslCtx) - private val group: EventExecutorGroup = DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors()) + private class ServerInitializer( + private val cfg: Configuration, + private val eventExecutorGroup: EventExecutorGroup + ) : ChannelInitializer() { companion object { + private fun createSslCtx(tls: Configuration.Tls): SslContext { + val keyStore = tls.keyStore + return if (keyStore == null) { + throw IllegalArgumentException("No keystore configured") + } else { + val javaKeyStore = loadKeystore(keyStore.file, keyStore.password) + val serverKey = javaKeyStore.getKey( + keyStore.keyAlias, keyStore.keyPassword?.let(String::toCharArray) + ) as PrivateKey + val serverCert: Array = + Arrays.stream(javaKeyStore.getCertificateChain(keyStore.keyAlias)) + .map { it as X509Certificate } + .toArray { size -> Array(size) { null } } + SslContextBuilder.forServer(serverKey, *serverCert).apply { + if (tls.isVerifyClients) { + clientAuth(ClientAuth.OPTIONAL) + val trustStore = tls.trustStore + if (trustStore != null) { + val ts = loadKeystore(trustStore.file, trustStore.password) + trustManager( + ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus) + ) + } + } + }.build() + } + } fun loadKeystore(file: Path, password: String?): KeyStore { val ext = JWO.splitExtension(file) @@ -235,6 +232,8 @@ class GradleBuildCacheServer(private val cfg: Configuration) { } } + private val sslContext: SslContext? = cfg.tls?.let(Companion::createSslCtx) + private fun userExtractor(authentication: Configuration.ClientCertificateAuthentication) = authentication.userExtractor?.let { extractor -> val pattern = Pattern.compile(extractor.pattern) @@ -294,7 +293,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { } val cacheImplementation = cfg.cache.materialize() val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/")) - pipeline.addLast(group, ServerHandler(cacheImplementation, prefix)) + pipeline.addLast(eventExecutorGroup, ServerHandler(cacheImplementation, prefix)) pipeline.addLast(ExceptionHandler()) } } @@ -446,8 +445,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { class ServerHandle( httpChannelFuture: ChannelFuture, - private val bossGroup: EventLoopGroup, - private val workerGroup: EventLoopGroup + private val executorGroups : Iterable ) : AutoCloseable { private val httpChannel: Channel = httpChannelFuture.channel() @@ -461,31 +459,32 @@ class GradleBuildCacheServer(private val cfg: Configuration) { try { closeFuture.sync() } finally { - val fut1 = workerGroup.shutdownGracefully() - val fut2 = if (bossGroup !== workerGroup) { - bossGroup.shutdownGracefully() - } else null - fut1.sync() - fut2?.sync() + executorGroups.forEach { + it.shutdownGracefully().sync() + } } } } fun run(): ServerHandle { // Create the multithreaded event loops for the server - val bossGroup = NioEventLoopGroup(0, Executors.defaultThreadFactory()) + val bossGroup = NioEventLoopGroup(0) val serverSocketChannel = NioServerSocketChannel::class.java - val workerGroup = if (cfg.isUseVirtualThread) { - NioEventLoopGroup(0, Executors.newVirtualThreadPerTaskExecutor()) - } else { - bossGroup + val workerGroup = bossGroup + val eventExecutorGroup = run { + val threadFactory = if(cfg.isUseVirtualThread) { + Thread.ofVirtual().factory() + } else { + null + } + DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory) } // A helper class that simplifies server configuration val bootstrap = ServerBootstrap().apply { // Configure the server group(bossGroup, workerGroup) channel(serverSocketChannel) - childHandler(ServerInitializer(cfg)) + childHandler(ServerInitializer(cfg, eventExecutorGroup)) option(ChannelOption.SO_BACKLOG, 128) childOption(ChannelOption.SO_KEEPALIVE, true) } @@ -494,7 +493,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { // Bind and start to accept incoming connections. val bindAddress = InetSocketAddress(cfg.host, cfg.port) val httpChannel = bootstrap.bind(bindAddress).sync() - return ServerHandle(httpChannel, bossGroup, workerGroup) + return ServerHandle(httpChannel, setOf(bossGroup, workerGroup, eventExecutorGroup)) } companion object { @@ -508,7 +507,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { return Parser.parse(doc) } - fun dumpConfiguration(conf : Configuration, outputStream: OutputStream) { + fun dumpConfiguration(conf: Configuration, outputStream: OutputStream) { Xml.write(Serializer.serialize(conf), outputStream) } } diff --git a/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt b/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt index 79591c0..f6ed628 100644 --- a/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt +++ b/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt @@ -32,7 +32,7 @@ object Parser { val serverPath = root.getAttribute("path") val useVirtualThread = root.getAttribute("useVirtualThreads") .takeIf(String::isNotEmpty) - ?.let(String::toBoolean) ?: false + ?.let(String::toBoolean) ?: true var authentication: Authentication? = null for (child in root.asIterable()) { when (child.localName) { diff --git a/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd b/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd index f051e7c..f302ac4 100644 --- a/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd +++ b/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd @@ -10,9 +10,6 @@ - - - @@ -28,7 +25,7 @@ - + diff --git a/src/test/kotlin/net/woggioni/gbcs/test/NoAuthServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/NoAuthServerTest.kt index d7e6bee..3700333 100644 --- a/src/test/kotlin/net/woggioni/gbcs/test/NoAuthServerTest.kt +++ b/src/test/kotlin/net/woggioni/gbcs/test/NoAuthServerTest.kt @@ -105,4 +105,28 @@ class NoAuthServerTest : AbstractServerTest() { val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode()) } + +// @Test +// @Order(4) +// fun manyRequestsTest() { +// val client: HttpClient = HttpClient.newHttpClient() +// +// for(i in 0 until 100000) { +// +// val newEntry = random.nextBoolean() +// val (key, _) = if(newEntry) { +// newEntry(random) +// } else { +// keyValuePair +// } +// val requestBuilder = newRequestBuilder(key).GET() +// +// val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) +// if(newEntry) { +// Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode()) +// } else { +// Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode()) +// } +// } +// } } \ No newline at end of file diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..c6f9111 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + + + + System.err + + %d [%highlight(%-5level)] \(%thread\) %logger{36} -%kvp- %msg %n + + + + + + + + + + \ No newline at end of file