Compare commits

..

1 Commits

Author SHA1 Message Date
ba961bd30d added optional even listener to client API 2025-01-27 23:55:59 +08:00
130 changed files with 1283 additions and 1802 deletions

View File

@@ -31,7 +31,7 @@ jobs:
username: woggioni username: woggioni
password: ${{ secrets.PUBLISHER_TOKEN }} password: ${{ secrets.PUBLISHER_TOKEN }}
- -
name: Build rbcs Docker image name: Build gbcs Docker image
uses: docker/build-push-action@v5.3.0 uses: docker/build-push-action@v5.3.0
with: with:
context: "docker/build/docker" context: "docker/build/docker"
@@ -39,12 +39,12 @@ jobs:
push: true push: true
pull: true pull: true
tags: | tags: |
gitea.woggioni.net/woggioni/rbcs:latest gitea.woggioni.net/woggioni/gbcs:latest
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }} gitea.woggioni.net/woggioni/gbcs:${{ steps.retrieve-version.outputs.VERSION }}
target: release target: release
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
- -
name: Build rbcs memcache Docker image name: Build gbcs memcached Docker image
uses: docker/build-push-action@v5.3.0 uses: docker/build-push-action@v5.3.0
with: with:
context: "docker/build/docker" context: "docker/build/docker"
@@ -52,11 +52,11 @@ jobs:
push: true push: true
pull: true pull: true
tags: | tags: |
gitea.woggioni.net/woggioni/rbcs:memcache gitea.woggioni.net/woggioni/gbcs:memcached
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }} gitea.woggioni.net/woggioni/gbcs:memcached-${{ steps.retrieve-version.outputs.VERSION }}
target: release-memcache target: release-memcached
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/rbcs:buildx cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/gbcs:buildx
- name: Publish artifacts - name: Publish artifacts
env: env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}

2
.gitignore vendored
View File

@@ -4,4 +4,4 @@
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build
rbcs-cli/native-image/*.json gbcs-cli/native-image/*.json

View File

@@ -15,7 +15,7 @@ allprojects { subproject ->
version = project.currentTag.map { it[0] }.get() version = project.currentTag.map { it[0] }.get()
} else { } else {
version = project.gitRevision.map { gitRevision -> version = project.gitRevision.map { gitRevision ->
"${getProperty('rbcs.version')}.${gitRevision[0..10]}" "${getProperty('gbcs.version')}.${gitRevision[0..10]}"
}.get() }.get()
} }

View File

@@ -4,13 +4,13 @@ USER luser
WORKDIR /home/luser WORKDIR /home/luser
FROM base-release AS release FROM base-release AS release
ADD rbcs-cli-envelope-*.jar rbcs.jar ADD gbcs-cli-envelope-*.jar gbcs.jar
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar", "server"] ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"]
FROM base-release AS release-memcache FROM base-release AS release-memcached
ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.jar ADD --chown=luser:luser gbcs-cli-envelope-*.jar gbcs.jar
RUN mkdir plugins RUN mkdir plugins
WORKDIR /home/luser/plugins WORKDIR /home/luser/plugins
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/gbcs-server-memcached*.tar
WORKDIR /home/luser WORKDIR /home/luser
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar", "server"] ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"]

View File

@@ -18,8 +18,8 @@ configurations {
} }
dependencies { dependencies {
docker project(path: ':rbcs-cli', configuration: 'release') docker project(path: ':gbcs-cli', configuration: 'release')
docker project(path: ':rbcs-server-memcache', configuration: 'release') docker project(path: ':gbcs-server-memcached', configuration: 'release')
} }
Provider<Task> cleanTaskProvider = tasks.named(BasePlugin.CLEAN_TASK_NAME) {} Provider<Task> cleanTaskProvider = tasks.named(BasePlugin.CLEAN_TASK_NAME) {}
@@ -35,33 +35,33 @@ Provider<Copy> prepareDockerBuild = tasks.register('prepareDockerBuild', Copy) {
Provider<DockerBuildImage> dockerBuild = tasks.register('dockerBuildImage', DockerBuildImage) { Provider<DockerBuildImage> dockerBuild = tasks.register('dockerBuildImage', DockerBuildImage) {
group = 'docker' group = 'docker'
dependsOn prepareDockerBuild dependsOn prepareDockerBuild
images.add('gitea.woggioni.net/woggioni/rbcs:latest') images.add('gitea.woggioni.net/woggioni/gbcs:latest')
images.add("gitea.woggioni.net/woggioni/rbcs:${version}") images.add("gitea.woggioni.net/woggioni/gbcs:${version}")
} }
Provider<DockerTagImage> dockerTag = tasks.register('dockerTagImage', DockerTagImage) { Provider<DockerTagImage> dockerTag = tasks.register('dockerTagImage', DockerTagImage) {
group = 'docker' group = 'docker'
repository = 'gitea.woggioni.net/woggioni/rbcs' repository = 'gitea.woggioni.net/woggioni/gbcs'
imageId = 'gitea.woggioni.net/woggioni/rbcs:latest' imageId = 'gitea.woggioni.net/woggioni/gbcs:latest'
tag = version tag = version
} }
Provider<DockerTagImage> dockerTagMemcache = tasks.register('dockerTagMemcacheImage', DockerTagImage) { Provider<DockerTagImage> dockerTagMemcached = tasks.register('dockerTagMemcachedImage', DockerTagImage) {
group = 'docker' group = 'docker'
repository = 'gitea.woggioni.net/woggioni/rbcs' repository = 'gitea.woggioni.net/woggioni/gbcs'
imageId = 'gitea.woggioni.net/woggioni/rbcs:memcache' imageId = 'gitea.woggioni.net/woggioni/gbcs:memcached'
tag = "${version}-memcache" tag = "${version}-memcached"
} }
Provider<DockerPushImage> dockerPush = tasks.register('dockerPushImage', DockerPushImage) { Provider<DockerPushImage> dockerPush = tasks.register('dockerPushImage', DockerPushImage) {
group = 'docker' group = 'docker'
dependsOn dockerTag, dockerTagMemcache dependsOn dockerTag, dockerTagMemcached
registryCredentials { registryCredentials {
url = getProperty('docker.registry.url') url = getProperty('docker.registry.url')
username = 'woggioni' username = 'woggioni'
password = System.getenv().get("PUBLISHER_TOKEN") password = System.getenv().get("PUBLISHER_TOKEN")
} }
images = [dockerTag.flatMap{ it.tag }, dockerTagMemcache.flatMap{ it.tag }] images = [dockerTag.flatMap{ it.tag }, dockerTagMemcached.flatMap{ it.tag }]
} }

View File

@@ -5,7 +5,6 @@ plugins {
} }
dependencies { dependencies {
api catalog.netty.buffer
} }
publishing { publishing {

View File

@@ -0,0 +1,6 @@
module net.woggioni.gbcs.api {
requires static lombok;
requires java.xml;
exports net.woggioni.gbcs.api;
exports net.woggioni.gbcs.api.exception;
}

View File

@@ -0,0 +1,12 @@
package net.woggioni.gbcs.api;
import net.woggioni.gbcs.api.exception.ContentTooLargeException;
import java.nio.channels.ReadableByteChannel;
public interface Cache extends AutoCloseable {
ReadableByteChannel get(String key);
void put(String key, byte[] content) throws ContentTooLargeException;
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.api; package net.woggioni.gbcs.api;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.api; package net.woggioni.gbcs.api;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -56,8 +56,7 @@ public class Configuration {
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
String name; String name;
Set<Role> roles; Set<Role> roles;
Quota groupQuota; Quota quota;
Quota userQuota;
} }
@Value @Value
@@ -135,7 +134,7 @@ public class Configuration {
} }
public interface Cache { public interface Cache {
net.woggioni.rbcs.api.Cache materialize(); net.woggioni.gbcs.api.Cache materialize();
String getNamespaceURI(); String getNamespaceURI();
String getTypeName(); String getTypeName();
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.api; package net.woggioni.gbcs.api;
public enum Role { public enum Role {
Reader, Writer Reader, Writer

View File

@@ -1,6 +1,6 @@
package net.woggioni.rbcs.api.exception; package net.woggioni.gbcs.api.exception;
public class CacheException extends RbcsException { public class CacheException extends GbcsException {
public CacheException(String message, Throwable cause) { public CacheException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

View File

@@ -1,6 +1,6 @@
package net.woggioni.rbcs.api.exception; package net.woggioni.gbcs.api.exception;
public class ConfigurationException extends RbcsException { public class ConfigurationException extends GbcsException {
public ConfigurationException(String message, Throwable cause) { public ConfigurationException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

View File

@@ -1,6 +1,6 @@
package net.woggioni.rbcs.api.exception; package net.woggioni.gbcs.api.exception;
public class ContentTooLargeException extends RbcsException { public class ContentTooLargeException extends GbcsException {
public ContentTooLargeException(String message, Throwable cause) { public ContentTooLargeException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

View File

@@ -0,0 +1,7 @@
package net.woggioni.gbcs.api.exception;
public class GbcsException extends RuntimeException {
public GbcsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -17,9 +17,9 @@ import net.woggioni.gradle.graalvm.JlinkPlugin
import net.woggioni.gradle.graalvm.JlinkTask import net.woggioni.gradle.graalvm.JlinkTask
Property<String> mainModuleName = objects.property(String.class) Property<String> mainModuleName = objects.property(String.class)
mainModuleName.set('net.woggioni.rbcs.cli') mainModuleName.set('net.woggioni.gbcs.cli')
Property<String> mainClassName = objects.property(String.class) Property<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.rbcs.cli.RemoteBuildCacheServerCli') mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.javaModuleMainClass = mainClassName options.javaModuleMainClass = mainClassName
@@ -44,10 +44,11 @@ envelopeJar {
dependencies { dependencies {
implementation catalog.jwo implementation catalog.jwo
implementation catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.netty.codec.http
implementation catalog.picocli implementation catalog.picocli
implementation project(':rbcs-client') implementation project(':gbcs-client')
implementation project(':rbcs-server') implementation project(':gbcs-server')
// runtimeOnly catalog.slf4j.jdk14 // runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic runtimeOnly catalog.logback.classic
@@ -55,10 +56,10 @@ dependencies {
} }
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) { Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.rbcs.LoggingConfig' // systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
// systemProperties['log.config.source'] = 'net/woggioni/rbcs/cli/logging.properties' // systemProperties['log.config.source'] = 'net/woggioni/gbcs/cli/logging.properties'
// systemProperties['java.util.logging.config.file'] = 'classpath:net/woggioni/rbcs/cli/logging.properties' // systemProperties['java.util.logging.config.file'] = 'classpath:net/woggioni/gbcs/cli/logging.properties'
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/rbcs/cli/logback.xml' systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/cli/logback.xml'
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED' systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED'
// systemProperties['org.slf4j.simpleLogger.showDateTime'] = 'true' // systemProperties['org.slf4j.simpleLogger.showDateTime'] = 'true'
@@ -82,7 +83,7 @@ tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) { tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
mainClass = mainClassName mainClass = mainClassName
mainModule = 'net.woggioni.rbcs.cli' mainModule = 'net.woggioni.gbcs.cli'
} }
artifacts { artifacts {

View File

@@ -0,0 +1,18 @@
module net.woggioni.gbcs.cli {
requires org.slf4j;
requires net.woggioni.gbcs.server;
requires info.picocli;
requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.client;
requires kotlin.stdlib;
requires net.woggioni.jwo;
requires net.woggioni.gbcs.api;
requires io.netty.codec.http;
exports net.woggioni.gbcs.cli.impl.converters to info.picocli;
opens net.woggioni.gbcs.cli.impl.commands to info.picocli;
opens net.woggioni.gbcs.cli.impl to info.picocli;
opens net.woggioni.gbcs.cli to info.picocli, net.woggioni.gbcs.common;
exports net.woggioni.gbcs.cli;
}

View File

@@ -0,0 +1,64 @@
package net.woggioni.gbcs.cli
import net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.cli.impl.AbstractVersionProvider
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.PasswordHashCommand
import net.woggioni.gbcs.cli.impl.commands.PutCommand
import net.woggioni.gbcs.cli.impl.commands.ServerCommand
import net.woggioni.jwo.Application
import picocli.CommandLine
import picocli.CommandLine.Model.CommandSpec
import java.net.URI
@CommandLine.Command(
name = "gbcs", versionProvider = GradleBuildCacheServerCli.VersionProvider::class
)
class GradleBuildCacheServerCli : GbcsCommand() {
class VersionProvider : AbstractVersionProvider()
companion object {
@JvmStatic
fun main(vararg args: String) {
Thread.currentThread().contextClassLoader = GradleBuildCacheServerCli::class.java.classLoader
GbcsUrlStreamHandlerFactory.install()
val log = contextLogger()
val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
.configurationDirectoryPropertyKey("net.woggioni.gbcs.conf.dir")
.build()
val gbcsCli = GradleBuildCacheServerCli()
val commandLine = CommandLine(gbcsCli)
commandLine.setExecutionExceptionHandler { ex, cl, parseResult ->
log.error(ex.message, ex)
CommandLine.ExitCode.SOFTWARE
}
commandLine.addSubcommand(ServerCommand(app))
commandLine.addSubcommand(PasswordHashCommand())
commandLine.addSubcommand(
CommandLine(ClientCommand(app)).apply {
addSubcommand(BenchmarkCommand())
addSubcommand(PutCommand())
addSubcommand(GetCommand())
})
System.exit(commandLine.execute(*args))
}
}
@CommandLine.Option(names = ["-V", "--version"], versionHelp = true)
var versionHelp = false
private set
@CommandLine.Spec
private lateinit var spec: CommandSpec
override fun run() {
spec.commandLine().usage(System.out);
}
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.cli.impl package net.woggioni.gbcs.cli.impl
import picocli.CommandLine import picocli.CommandLine
import java.util.jar.Attributes import java.util.jar.Attributes

View File

@@ -1,11 +1,11 @@
package net.woggioni.rbcs.cli.impl package net.woggioni.gbcs.cli.impl
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import picocli.CommandLine import picocli.CommandLine
import java.nio.file.Path import java.nio.file.Path
abstract class RbcsCommand : Runnable { abstract class GbcsCommand : Runnable {
@CommandLine.Option(names = ["-h", "--help"], usageHelp = true) @CommandLine.Option(names = ["-h", "--help"], usageHelp = true)
var usageHelp = false var usageHelp = false

View File

@@ -0,0 +1,170 @@
package net.woggioni.gbcs.cli.impl.commands
import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.FullHttpResponse
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.error
import net.woggioni.gbcs.common.info
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GradleBuildCacheClient
import net.woggioni.gbcs.client.RequestEventListener
import net.woggioni.jwo.JWO
import picocli.CommandLine
import java.security.SecureRandom
import java.time.Duration
import java.time.Instant
import java.util.Base64
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.Semaphore
import java.util.concurrent.atomic.AtomicLong
import kotlin.random.Random
@CommandLine.Command(
name = "benchmark",
description = ["Run a load test against the server"],
showDefaultValues = true
)
class BenchmarkCommand : GbcsCommand() {
private val log = contextLogger()
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
@CommandLine.Option(
names = ["-e", "--entries"],
description = ["Total number of elements to be added to the cache"],
paramLabel = "NUMBER_OF_ENTRIES"
)
private var numberOfEntries = 1000
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")
}
val client = GradleBuildCacheClient(profile)
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(0x1000, { _ -> content })
yield(key to value)
}
}
log.info {
"Starting insertion"
}
val entries = let {
val completionCounter = AtomicLong(0)
val completionQueue = LinkedBlockingQueue<Pair<String, ByteArray>>(numberOfEntries)
val start = Instant.now()
val semaphore = Semaphore(profile.maxConnections * 3)
val totalElapsedTime = AtomicLong(0)
val iterator = entryGenerator.take(numberOfEntries).iterator()
while(completionCounter.get() < numberOfEntries) {
if(iterator.hasNext()) {
val entry = iterator.next()
semaphore.acquire()
val eventListener = object : RequestEventListener {
var start: Long? = null
override fun requestSent(req: FullHttpRequest) {
this.start = System.nanoTime()
}
override fun responseReceived(res: FullHttpResponse) {
this.start?.let { requestStart ->
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
}
this.start = null
}
}
val future = client.put(entry.first, entry.second, eventListener).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"
}
log.info {
val avgTxTime = String.format("%.0f", totalElapsedTime.get() / numberOfEntries.toDouble() / 1e6)
"Average time per insertion: $avgTxTime ms"
}
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()
val totalElapsedTime = AtomicLong(0)
entries.forEach { entry ->
semaphore.acquire()
val eventListener = object : RequestEventListener {
var start : Long? = null
override fun requestSent(req: FullHttpRequest) {
this.start = System.nanoTime()
}
override fun responseReceived(res: FullHttpResponse) {
this.start?.let { requestStart ->
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
}
this.start = null
}
}
val future = client.get(entry.first, eventListener).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"
}
log.info {
val avgTxTime = String.format("%.0f", totalElapsedTime.get() / completionCounter.toDouble() / 1e6)
"Average time per retrieval: $avgTxTime ms"
}
} else {
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
}
}
}

View File

@@ -1,24 +1,24 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.gbcs.client.GradleBuildCacheClient
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import picocli.CommandLine import picocli.CommandLine
import java.nio.file.Path import java.nio.file.Path
@CommandLine.Command( @CommandLine.Command(
name = "client", name = "client",
description = ["RBCS client"], description = ["GBCS client"],
showDefaultValues = true showDefaultValues = true
) )
class ClientCommand(app : Application) : RbcsCommand() { class ClientCommand(app : Application) : GbcsCommand() {
@CommandLine.Option( @CommandLine.Option(
names = ["-c", "--configuration"], names = ["-c", "--configuration"],
description = ["Path to the client configuration file"], description = ["Path to the client configuration file"],
paramLabel = "CONFIGURATION_FILE" paramLabel = "CONFIGURATION_FILE"
) )
private var configurationFile : Path = findConfigurationFile(app, "rbcs-client.xml") private var configurationFile : Path = findConfigurationFile(app, "gbcs-client.xml")
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--profile"], names = ["-p", "--profile"],
@@ -28,8 +28,8 @@ class ClientCommand(app : Application) : RbcsCommand() {
) )
var profileName : String? = null var profileName : String? = null
val configuration : RemoteBuildCacheClient.Configuration by lazy { val configuration : GradleBuildCacheClient.Configuration by lazy {
RemoteBuildCacheClient.Configuration.parse(configurationFile) GradleBuildCacheClient.Configuration.parse(configurationFile)
} }
override fun run() { override fun run() {

View File

@@ -1,8 +1,8 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.client.GradleBuildCacheClient
import picocli.CommandLine import picocli.CommandLine
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@@ -12,7 +12,7 @@ import java.nio.file.Path
description = ["Fetch a value from the cache with the specified key"], description = ["Fetch a value from the cache with the specified key"],
showDefaultValues = true showDefaultValues = true
) )
class GetCommand : RbcsCommand() { class GetCommand : GbcsCommand() {
private val log = contextLogger() private val log = contextLogger()
@CommandLine.Spec @CommandLine.Spec
@@ -38,7 +38,7 @@ class GetCommand : RbcsCommand() {
clientCommand.configuration.profiles[profileName] clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration") ?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
} }
RemoteBuildCacheClient(profile).use { client -> GradleBuildCacheClient(profile).use { client ->
client.get(key).thenApply { value -> client.get(key).thenApply { value ->
value?.let { value?.let {
(output?.let(Files::newOutputStream) ?: System.out).use { (output?.let(Files::newOutputStream) ?: System.out).use {

View File

@@ -1,8 +1,8 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter
import net.woggioni.jwo.UncloseableOutputStream import net.woggioni.jwo.UncloseableOutputStream
import picocli.CommandLine import picocli.CommandLine
import java.io.OutputStream import java.io.OutputStream
@@ -12,10 +12,10 @@ import java.io.PrintWriter
@CommandLine.Command( @CommandLine.Command(
name = "password", name = "password",
description = ["Generate a password hash to add to RBCS configuration file"], description = ["Generate a password hash to add to GBCS configuration file"],
showDefaultValues = true showDefaultValues = true
) )
class PasswordHashCommand : RbcsCommand() { class PasswordHashCommand : GbcsCommand() {
@CommandLine.Option( @CommandLine.Option(
names = ["-o", "--output-file"], names = ["-o", "--output-file"],
description = ["Write the output to a file instead of stdout"], description = ["Write the output to a file instead of stdout"],

View File

@@ -1,9 +1,9 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.cli.impl.converters.InputStreamConverter import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.gbcs.cli.impl.converters.InputStreamConverter
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.client.GradleBuildCacheClient
import picocli.CommandLine import picocli.CommandLine
import java.io.InputStream import java.io.InputStream
@@ -12,7 +12,7 @@ import java.io.InputStream
description = ["Add or replace a value to the cache with the specified key"], description = ["Add or replace a value to the cache with the specified key"],
showDefaultValues = true showDefaultValues = true
) )
class PutCommand : RbcsCommand() { class PutCommand : GbcsCommand() {
private val log = contextLogger() private val log = contextLogger()
@CommandLine.Spec @CommandLine.Spec
@@ -39,7 +39,7 @@ class PutCommand : RbcsCommand() {
clientCommand.configuration.profiles[profileName] clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration") ?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
} }
RemoteBuildCacheClient(profile).use { client -> GradleBuildCacheClient(profile).use { client ->
value.use { value.use {
client.put(key, it.readAllBytes()) client.put(key, it.readAllBytes())
}.get() }.get()

View File

@@ -1,26 +1,25 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.gbcs.server.GradleBuildCacheServer
import net.woggioni.rbcs.cli.impl.converters.DurationConverter import net.woggioni.gbcs.server.GradleBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.common.debug import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.common.info import net.woggioni.gbcs.common.debug
import net.woggioni.rbcs.server.RemoteBuildCacheServer import net.woggioni.gbcs.common.info
import net.woggioni.rbcs.server.RemoteBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import picocli.CommandLine import picocli.CommandLine
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration
@CommandLine.Command( @CommandLine.Command(
name = "server", name = "server",
description = ["RBCS server"], description = ["GBCS server"],
showDefaultValues = true showDefaultValues = true
) )
class ServerCommand(app : Application) : RbcsCommand() { class ServerCommand(app : Application) : GbcsCommand() {
private val log = contextLogger() private val log = contextLogger()
@@ -36,20 +35,16 @@ class ServerCommand(app : Application) : RbcsCommand() {
} }
} }
@CommandLine.Option(
names = ["-t", "--timeout"],
description = ["Exit after the specified time"],
paramLabel = "TIMEOUT",
converter = [DurationConverter::class]
)
private var timeout: Duration? = null
@CommandLine.Option( @CommandLine.Option(
names = ["-c", "--config-file"], names = ["-c", "--config-file"],
description = ["Read the application configuration from this file"], description = ["Read the application configuration from this file"],
paramLabel = "CONFIG_FILE" paramLabel = "CONFIG_FILE"
) )
private var configurationFile: Path = findConfigurationFile(app, "rbcs-server.xml") private var configurationFile: Path = findConfigurationFile(app, "gbcs-server.xml")
val configuration : Configuration by lazy {
GradleBuildCacheServer.loadConfiguration(configurationFile)
}
override fun run() { override fun run() {
if (!Files.exists(configurationFile)) { if (!Files.exists(configurationFile)) {
@@ -57,20 +52,16 @@ class ServerCommand(app : Application) : RbcsCommand() {
createDefaultConfigurationFile(configurationFile) createDefaultConfigurationFile(configurationFile)
} }
val configuration = RemoteBuildCacheServer.loadConfiguration(configurationFile) val configuration = GradleBuildCacheServer.loadConfiguration(configurationFile)
log.debug { log.debug {
ByteArrayOutputStream().also { ByteArrayOutputStream().also {
RemoteBuildCacheServer.dumpConfiguration(configuration, it) GradleBuildCacheServer.dumpConfiguration(configuration, it)
}.let { }.let {
"Server configuration:\n${String(it.toByteArray())}" "Server configuration:\n${String(it.toByteArray())}"
} }
} }
val server = RemoteBuildCacheServer(configuration) val server = GradleBuildCacheServer(configuration)
server.run().use { server -> server.run().use {
timeout?.let {
Thread.sleep(it)
server.shutdown()
}
} }
} }
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.cli.impl.converters package net.woggioni.gbcs.cli.impl.converters
import picocli.CommandLine import picocli.CommandLine
import java.io.InputStream import java.io.InputStream

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.cli.impl.converters package net.woggioni.gbcs.cli.impl.converters
import picocli.CommandLine import picocli.CommandLine
import java.io.OutputStream import java.io.OutputStream

View File

@@ -15,4 +15,6 @@
<root level="info"> <root level="info">
<appender-ref ref="console"/> <appender-ref ref="console"/>
</root> </root>
<logger name="com.google.code.yanf4j" level="warn"/>
<logger name="net.rubyeye.xmemcached" level="warn"/>
</configuration> </configuration>

View File

@@ -4,13 +4,11 @@ plugins {
} }
dependencies { dependencies {
implementation project(':rbcs-api') implementation project(':gbcs-api')
implementation project(':rbcs-common') implementation project(':gbcs-common')
implementation catalog.picocli
implementation catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.netty.buffer implementation catalog.netty.buffer
implementation catalog.netty.handler
implementation catalog.netty.transport
implementation catalog.netty.common
implementation catalog.netty.codec.http implementation catalog.netty.codec.http
testRuntimeOnly catalog.logback.classic testRuntimeOnly catalog.logback.classic

View File

@@ -1,4 +1,4 @@
module net.woggioni.rbcs.client { module net.woggioni.gbcs.client {
requires io.netty.handler; requires io.netty.handler;
requires io.netty.codec.http; requires io.netty.codec.http;
requires io.netty.transport; requires io.netty.transport;
@@ -6,12 +6,12 @@ module net.woggioni.rbcs.client {
requires io.netty.common; requires io.netty.common;
requires io.netty.buffer; requires io.netty.buffer;
requires java.xml; requires java.xml;
requires net.woggioni.rbcs.common; requires net.woggioni.gbcs.common;
requires net.woggioni.rbcs.api; requires net.woggioni.gbcs.api;
requires io.netty.codec; requires io.netty.codec;
requires org.slf4j; requires org.slf4j;
exports net.woggioni.rbcs.client; exports net.woggioni.gbcs.client;
opens net.woggioni.rbcs.client.schema; opens net.woggioni.gbcs.client.schema;
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.client package net.woggioni.gbcs.client
import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
@@ -30,11 +30,11 @@ import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.stream.ChunkedWriteHandler import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.Future import io.netty.util.concurrent.Future
import io.netty.util.concurrent.GenericFutureListener import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.rbcs.client.impl.Parser import net.woggioni.gbcs.client.impl.Parser
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.common.debug import net.woggioni.gbcs.common.debug
import net.woggioni.rbcs.common.trace import net.woggioni.gbcs.common.trace
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.nio.file.Files import java.nio.file.Files
@@ -45,10 +45,10 @@ import java.time.Duration
import java.util.Base64 import java.util.Base64
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.random.Random
import io.netty.util.concurrent.Future as NettyFuture import io.netty.util.concurrent.Future as NettyFuture
class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
private val group: NioEventLoopGroup private val group: NioEventLoopGroup
private var sslContext: SslContext private var sslContext: SslContext
private val log = contextLogger() private val log = contextLogger()
@@ -206,7 +206,6 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
retryPolicy.initialDelayMillis.toDouble(), retryPolicy.initialDelayMillis.toDouble(),
retryPolicy.exp, retryPolicy.exp,
outcomeHandler, outcomeHandler,
Random.Default,
operation operation
) )
} else { } else {
@@ -214,28 +213,9 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
} }
} }
fun healthCheck(nonce: ByteArray): CompletableFuture<ByteArray?> { fun get(key: String, eventListener : RequestEventListener? = null): CompletableFuture<ByteArray?> {
return executeWithRetry { return executeWithRetry {
sendRequest(profile.serverURI, HttpMethod.TRACE, nonce) sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null, eventListener)
}.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<ByteArray?> {
return executeWithRetry {
sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)
}.thenApply { }.thenApply {
val status = it.status() val status = it.status()
if (it.status() == HttpResponseStatus.NOT_FOUND) { if (it.status() == HttpResponseStatus.NOT_FOUND) {
@@ -254,9 +234,9 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
} }
} }
fun put(key: String, content: ByteArray): CompletableFuture<Unit> { fun put(key: String, content: ByteArray, eventListener : RequestEventListener? = null): CompletableFuture<Unit> {
return executeWithRetry { return executeWithRetry {
sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content) sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content, eventListener)
}.thenApply { }.thenApply {
val status = it.status() val status = it.status()
if (it.status() != HttpResponseStatus.CREATED && it.status() != HttpResponseStatus.OK) { if (it.status() != HttpResponseStatus.CREATED && it.status() != HttpResponseStatus.OK) {
@@ -265,7 +245,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
} }
} }
private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?): CompletableFuture<FullHttpResponse> { private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?, eventListener : RequestEventListener?): CompletableFuture<FullHttpResponse> {
val responseFuture = CompletableFuture<FullHttpResponse>() val responseFuture = CompletableFuture<FullHttpResponse>()
// Custom handler for processing responses // Custom handler for processing responses
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> { pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
@@ -281,6 +261,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
pipeline.removeLast() pipeline.removeLast()
pool.release(channel) pool.release(channel)
responseFuture.complete(response) responseFuture.complete(response)
eventListener?.responseReceived(response)
} }
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
@@ -326,7 +307,11 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
// Set headers // Set headers
// Send the request // Send the request
channel.writeAndFlush(request) channel.writeAndFlush(request).addListener {
if(it.isSuccess) {
eventListener?.requestSent(request)
}
}
} else { } else {
responseFuture.completeExceptionally(channelFuture.cause()) responseFuture.completeExceptionally(channelFuture.cause())
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.client package net.woggioni.gbcs.client
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus

View File

@@ -0,0 +1,10 @@
package net.woggioni.gbcs.client
import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.FullHttpResponse
interface RequestEventListener {
fun requestSent(req : FullHttpRequest) {}
fun responseReceived(res : FullHttpResponse) {}
fun exceptionCaught(ex : Throwable) {}
}

View File

@@ -1,9 +1,9 @@
package net.woggioni.rbcs.client.impl package net.woggioni.gbcs.client.impl
import net.woggioni.rbcs.api.exception.ConfigurationException import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.gbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.asIterable import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.gbcs.client.GradleBuildCacheClient
import org.w3c.dom.Document import org.w3c.dom.Document
import java.net.URI import java.net.URI
import java.nio.file.Files import java.nio.file.Files
@@ -15,9 +15,9 @@ import java.time.Duration
object Parser { object Parser {
fun parse(document: Document): RemoteBuildCacheClient.Configuration { fun parse(document: Document): GradleBuildCacheClient.Configuration {
val root = document.documentElement val root = document.documentElement
val profiles = mutableMapOf<String, RemoteBuildCacheClient.Configuration.Profile>() val profiles = mutableMapOf<String, GradleBuildCacheClient.Configuration.Profile>()
for (child in root.asIterable()) { for (child in root.asIterable()) {
val tagName = child.localName val tagName = child.localName
@@ -27,8 +27,8 @@ object Parser {
child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required") child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
val uri = child.renderAttribute("base-url")?.let(::URI) val uri = child.renderAttribute("base-url")?.let(::URI)
?: throw ConfigurationException("base-url attribute is required") ?: throw ConfigurationException("base-url attribute is required")
var authentication: RemoteBuildCacheClient.Configuration.Authentication? = null var authentication: GradleBuildCacheClient.Configuration.Authentication? = null
var retryPolicy: RemoteBuildCacheClient.Configuration.RetryPolicy? = null var retryPolicy: GradleBuildCacheClient.Configuration.RetryPolicy? = null
for (gchild in child.asIterable()) { for (gchild in child.asIterable()) {
when (gchild.localName) { when (gchild.localName) {
"tls-client-auth" -> { "tls-client-auth" -> {
@@ -49,7 +49,7 @@ object Parser {
.toList() .toList()
.toTypedArray() .toTypedArray()
authentication = authentication =
RemoteBuildCacheClient.Configuration.Authentication.TlsClientAuthenticationCredentials( GradleBuildCacheClient.Configuration.Authentication.TlsClientAuthenticationCredentials(
key, key,
certChain certChain
) )
@@ -61,7 +61,7 @@ object Parser {
val password = gchild.renderAttribute("password") val password = gchild.renderAttribute("password")
?: throw ConfigurationException("password attribute is required") ?: throw ConfigurationException("password attribute is required")
authentication = authentication =
RemoteBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials( GradleBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials(
username, username,
password password
) )
@@ -80,7 +80,7 @@ object Parser {
gchild.renderAttribute("exp") gchild.renderAttribute("exp")
?.let(String::toDouble) ?.let(String::toDouble)
?: 2.0f ?: 2.0f
retryPolicy = RemoteBuildCacheClient.Configuration.RetryPolicy( retryPolicy = GradleBuildCacheClient.Configuration.RetryPolicy(
maxAttempts, maxAttempts,
initialDelay.toMillis(), initialDelay.toMillis(),
exp.toDouble() exp.toDouble()
@@ -93,7 +93,7 @@ object Parser {
?: 50 ?: 50
val connectionTimeout = child.renderAttribute("connection-timeout") val connectionTimeout = child.renderAttribute("connection-timeout")
?.let(Duration::parse) ?.let(Duration::parse)
profiles[name] = RemoteBuildCacheClient.Configuration.Profile( profiles[name] = GradleBuildCacheClient.Configuration.Profile(
uri, uri,
authentication, authentication,
connectionTimeout, connectionTimeout,
@@ -103,6 +103,6 @@ object Parser {
} }
} }
} }
return RemoteBuildCacheClient.Configuration(profiles) return GradleBuildCacheClient.Configuration(profiles)
} }
} }

View File

@@ -1,10 +1,8 @@
package net.woggioni.rbcs.client package net.woggioni.gbcs.client
import io.netty.util.concurrent.EventExecutorGroup import io.netty.util.concurrent.EventExecutorGroup
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.pow
import kotlin.random.Random
sealed class OperationOutcome<T> { sealed class OperationOutcome<T> {
class Success<T>(val result: T) : OperationOutcome<T>() class Success<T>(val result: T) : OperationOutcome<T>()
@@ -26,10 +24,8 @@ fun <T> executeWithRetry(
initialDelay: Double, initialDelay: Double,
exp: Double, exp: Double,
outcomeHandler: OutcomeHandler<T>, outcomeHandler: OutcomeHandler<T>,
randomizer : Random?,
cb: () -> CompletableFuture<T> cb: () -> CompletableFuture<T>
): CompletableFuture<T> { ): CompletableFuture<T> {
val finalResult = cb() val finalResult = cb()
var future = finalResult var future = finalResult
var shortCircuit = false var shortCircuit = false
@@ -50,7 +46,7 @@ fun <T> executeWithRetry(
is OutcomeHandlerResult.Retry -> { is OutcomeHandlerResult.Retry -> {
val res = CompletableFuture<T>() val res = CompletableFuture<T>()
val delay = run { val delay = run {
val scheduledDelay = (initialDelay * exp.pow(i.toDouble()) * (1.0 + (randomizer?.nextDouble(-0.5, 0.5) ?: 0.0))).toLong() val scheduledDelay = (initialDelay * Math.pow(exp, i.toDouble())).toLong()
outcomeHandlerResult.suggestedDelayMillis?.coerceAtMost(scheduledDelay) ?: scheduledDelay outcomeHandlerResult.suggestedDelayMillis?.coerceAtMost(scheduledDelay) ?: scheduledDelay
} }
eventExecutorGroup.schedule({ eventExecutorGroup.schedule({

View File

@@ -1,25 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.rbcs.client" <xs:schema targetNamespace="urn:net.woggioni.gbcs.client"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:rbcs-client="urn:net.woggioni.rbcs.client" xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
elementFormDefault="unqualified" elementFormDefault="unqualified"
> >
<xs:element name="profiles" type="rbcs-client:profilesType"/> <xs:element name="profiles" type="gbcs-client:profilesType"/>
<xs:complexType name="profilesType"> <xs:complexType name="profilesType">
<xs:sequence minOccurs="0"> <xs:sequence minOccurs="0">
<xs:element name="profile" type="rbcs-client:profileType" maxOccurs="unbounded"/> <xs:element name="profile" type="gbcs-client:profileType" maxOccurs="unbounded"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
<xs:complexType name="profileType"> <xs:complexType name="profileType">
<xs:sequence> <xs:sequence>
<xs:choice> <xs:choice>
<xs:element name="no-auth" type="rbcs-client:noAuthType"/> <xs:element name="no-auth" type="gbcs-client:noAuthType"/>
<xs:element name="basic-auth" type="rbcs-client:basicAuthType"/> <xs:element name="basic-auth" type="gbcs-client:basicAuthType"/>
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType"/> <xs:element name="tls-client-auth" type="gbcs-client:tlsClientAuthType"/>
</xs:choice> </xs:choice>
<xs:element name="retry-policy" type="rbcs-client:retryType" minOccurs="0"/> <xs:element name="retry-policy" type="gbcs-client:retryType" minOccurs="0"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/> <xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="base-url" type="xs:anyURI" use="required"/> <xs:attribute name="base-url" type="xs:anyURI" use="required"/>

View File

@@ -1,9 +1,10 @@
package net.woggioni.rbcs.client package net.woggioni.gbcs.client
import io.netty.util.concurrent.DefaultEventExecutorGroup import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.common.contextLogger
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
@@ -89,7 +90,7 @@ class RetryTest {
val random = Random(testArgs.seed) val random = Random(testArgs.seed)
val future = val future =
executeWithRetry(executor, testArgs.maxAttempt, testArgs.initialDelay, testArgs.exp, outcomeHandler, null) { executeWithRetry(executor, testArgs.maxAttempt, testArgs.initialDelay, testArgs.exp, outcomeHandler) {
val now = System.nanoTime() val now = System.nanoTime()
val result = CompletableFuture<Int>() val result = CompletableFuture<Int>()
executor.submit { executor.submit {
@@ -129,7 +130,7 @@ class RetryTest {
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6 previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
val actualTimestamp = timestamp val actualTimestamp = timestamp
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
Assertions.assertTrue(err < 1e-2) Assertions.assertTrue(err < 1e-3)
} }
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) { if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
/* /*

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
xs:schemaLocation="urn:net.woggioni.gbcs.client jms://net.woggioni.gbcs.client/net/woggioni/gbcs/client/schema/gbcs-client.xsd"
>
<profile name="profile1" base-url="https://gbcs1.example.com/">
<tls-client-auth
key-store-file="keystore.pfx"
key-store-password="password"
key-alias="woggioni@c962475fa38"
key-password="key-password"/>
</profile>
<profile name="profile2" base-url="https://gbcs2.example.com/">
<basic-auth user="user" password="password"/>
</profile>
</gbcs-client:profiles>

View File

@@ -6,10 +6,9 @@ plugins {
} }
dependencies { dependencies {
implementation project(':rbcs-api') implementation project(':gbcs-api')
implementation catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.jwo implementation catalog.jwo
implementation catalog.netty.buffer
} }
publishing { publishing {

View File

@@ -1,11 +1,10 @@
module net.woggioni.rbcs.common { module net.woggioni.gbcs.common {
requires java.xml; requires java.xml;
requires java.logging; requires java.logging;
requires org.slf4j; requires org.slf4j;
requires kotlin.stdlib; requires kotlin.stdlib;
requires net.woggioni.jwo; requires net.woggioni.jwo;
requires io.netty.buffer;
provides java.net.spi.URLStreamHandlerProvider with net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory; provides java.net.spi.URLStreamHandlerProvider with net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory;
exports net.woggioni.rbcs.common; exports net.woggioni.gbcs.common;
} }

View File

@@ -0,0 +1,12 @@
package net.woggioni.gbcs.common
import java.net.URI
import java.net.URL
object GBCS {
fun String.toUrl() : URL = URL.of(URI(this), null)
const val GBCS_NAMESPACE_URI: String = "urn:net.woggioni.gbcs.server"
const val GBCS_PREFIX: String = "gbcs"
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
}

View File

@@ -1,18 +1,20 @@
package net.woggioni.rbcs.common package net.woggioni.gbcs.common
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.net.URL import java.net.URL
import java.net.URLConnection import java.net.URLConnection
import java.net.URLStreamHandler import java.net.URLStreamHandler
import java.net.URLStreamHandlerFactory
import java.net.spi.URLStreamHandlerProvider import java.net.spi.URLStreamHandlerProvider
import java.util.Optional
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.Collectors import java.util.stream.Collectors
class RbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() { class GbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
private class ClasspathHandler(private val classLoader: ClassLoader = RbcsUrlStreamHandlerFactory::class.java.classLoader) : private class ClasspathHandler(private val classLoader: ClassLoader = GbcsUrlStreamHandlerFactory::class.java.classLoader) :
URLStreamHandler() { URLStreamHandler() {
override fun openConnection(u: URL): URLConnection? { override fun openConnection(u: URL): URLConnection? {
@@ -35,17 +37,13 @@ class RbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
private class JpmsHandler : URLStreamHandler() { private class JpmsHandler : URLStreamHandler() {
override fun openConnection(u: URL): URLConnection { override fun openConnection(u: URL): URLConnection {
val moduleName = u.host
val thisModule = javaClass.module val thisModule = javaClass.module
val sourceModule = val sourceModule = Optional.ofNullable(thisModule)
thisModule .map { obj: Module -> obj.layer }
?.let(Module::getLayer) .flatMap { layer: ModuleLayer ->
?.let { layer: ModuleLayer -> val moduleName = u.host
layer.findModule(moduleName).orElse(null) layer.findModule(moduleName)
} ?: if(thisModule.layer == null) { }.orElse(thisModule)
thisModule
} else throw ModuleNotFoundException("Module '$moduleName' not found")
return JpmsResourceURLConnection(u, sourceModule) return JpmsResourceURLConnection(u, sourceModule)
} }
} }
@@ -56,9 +54,7 @@ class RbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
@Throws(IOException::class) @Throws(IOException::class)
override fun getInputStream(): InputStream { override fun getInputStream(): InputStream {
val resource = getURL().path return module.getResourceAsStream(getURL().path)
return module.getResourceAsStream(resource)
?: throw ResourceNotFoundException("Resource '$resource' not found in module '${module.name}'")
} }
} }
@@ -87,12 +83,12 @@ class RbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
private val installed = AtomicBoolean(false) private val installed = AtomicBoolean(false)
fun install() { fun install() {
if (!installed.getAndSet(true)) { if (!installed.getAndSet(true)) {
URL.setURLStreamHandlerFactory(RbcsUrlStreamHandlerFactory()) URL.setURLStreamHandlerFactory(GbcsUrlStreamHandlerFactory())
} }
} }
private val packageMap: Map<String, List<Module>> by lazy { private val packageMap: Map<String, List<Module>> by lazy {
RbcsUrlStreamHandlerFactory::class.java.module.layer GbcsUrlStreamHandlerFactory::class.java.module.layer
.modules() .modules()
.stream() .stream()
.flatMap { m: Module -> .flatMap { m: Module ->

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.common package net.woggioni.gbcs.common
data class HostAndPort(val host: String, val port: Int = 0) { data class HostAndPort(val host: String, val port: Int = 0) {

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.common package net.woggioni.gbcs.common
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.common package net.woggioni.gbcs.common
import java.security.SecureRandom import java.security.SecureRandom
import java.security.spec.KeySpec import java.security.spec.KeySpec

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.common package net.woggioni.gbcs.common
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory

View File

@@ -0,0 +1 @@
net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory

View File

@@ -6,10 +6,10 @@ plugins {
configurations { configurations {
bundle { bundle {
extendsFrom runtimeClasspath
canBeResolved = true canBeResolved = true
canBeConsumed = false canBeConsumed = false
visible = false visible = false
transitive = false
resolutionStrategy { resolutionStrategy {
dependencies { dependencies {
@@ -29,20 +29,10 @@ configurations {
} }
dependencies { dependencies {
implementation project(':rbcs-common') compileOnly project(':gbcs-common')
implementation project(':rbcs-api') compileOnly project(':gbcs-api')
implementation catalog.jwo compileOnly catalog.jwo
implementation catalog.slf4j.api implementation catalog.xmemcached
implementation catalog.netty.common
implementation catalog.netty.codec.memcache
bundle catalog.netty.codec.memcache
testRuntimeOnly catalog.logback.classic
}
tasks.named(JavaPlugin.TEST_TASK_NAME, Test) {
systemProperty("io.netty.leakDetectionLevel", "PARANOID")
} }
Provider<Tar> bundleTask = tasks.register("bundle", Tar) { Provider<Tar> bundleTask = tasks.register("bundle", Tar) {

View File

@@ -0,0 +1,14 @@
import net.woggioni.gbcs.api.CacheProvider;
module net.woggioni.gbcs.server.memcached {
requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.api;
requires com.googlecode.xmemcached;
requires net.woggioni.jwo;
requires java.xml;
requires kotlin.stdlib;
provides CacheProvider with net.woggioni.gbcs.server.memcached.MemcachedCacheProvider;
opens net.woggioni.gbcs.server.memcached.schema;
}

View File

@@ -0,0 +1,59 @@
package net.woggioni.gbcs.server.memcached
import net.rubyeye.xmemcached.XMemcachedClientBuilder
import net.rubyeye.xmemcached.command.BinaryCommandFactory
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.rubyeye.xmemcached.transcoders.SerializingTranscoder
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.api.exception.ContentTooLargeException
import net.woggioni.gbcs.common.HostAndPort
import net.woggioni.jwo.JWO
import java.io.ByteArrayInputStream
import java.net.InetSocketAddress
import java.nio.channels.Channels
import java.nio.channels.ReadableByteChannel
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.time.Duration
class MemcachedCache(
servers: List<HostAndPort>,
private val maxAge: Duration,
maxSize : Int,
digestAlgorithm: String?,
compressionMode: CompressionMode,
) : Cache {
private val memcachedClient = XMemcachedClientBuilder(
servers.stream().map { addr: HostAndPort -> InetSocketAddress(addr.host, addr.port) }.toList()
).apply {
commandFactory = BinaryCommandFactory()
digestAlgorithm?.let { dAlg ->
setKeyProvider { key ->
val md = MessageDigest.getInstance(dAlg)
md.update(key.toByteArray(StandardCharsets.UTF_8))
JWO.bytesToHex(md.digest())
}
}
transcoder = SerializingTranscoder(maxSize).apply {
setCompressionMode(compressionMode)
}
}.build()
override fun get(key: String): ReadableByteChannel? {
return memcachedClient.get<ByteArray>(key)
?.let(::ByteArrayInputStream)
?.let(Channels::newChannel)
}
override fun put(key: String, content: ByteArray) {
try {
memcachedClient[key, maxAge.toSeconds().toInt()] = content
} catch (e: IllegalArgumentException) {
throw ContentTooLargeException(e.message, e)
}
}
override fun close() {
memcachedClient.shutdown()
}
}

View File

@@ -0,0 +1,26 @@
package net.woggioni.gbcs.server.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.common.HostAndPort
import java.time.Duration
data class MemcachedCacheConfiguration(
var servers: List<HostAndPort>,
var maxAge: Duration = Duration.ofDays(1),
var maxSize: Int = 0x100000,
var digestAlgorithm: String? = null,
var compressionMode: CompressionMode = CompressionMode.ZIP,
) : Configuration.Cache {
override fun materialize() = MemcachedCache(
servers,
maxAge,
maxSize,
digestAlgorithm,
compressionMode
)
override fun getNamespaceURI() = "urn:net.woggioni.gbcs.server.memcached"
override fun getTypeName() = "memcachedCacheType"
}

View File

@@ -0,0 +1,88 @@
package net.woggioni.gbcs.server.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.common.GBCS
import net.woggioni.gbcs.common.HostAndPort
import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.common.Xml.Companion.asIterable
import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.time.Duration
class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
override fun getXmlSchemaLocation() = "jpms://net.woggioni.gbcs.server.memcached/net/woggioni/gbcs/server/memcached/schema/gbcs-memcached.xsd"
override fun getXmlType() = "memcachedCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.gbcs.server.memcached"
val xmlNamespacePrefix : String
get() = "gbcs-memcached"
override fun deserialize(el: Element): MemcachedCacheConfiguration {
val servers = mutableListOf<HostAndPort>()
val maxAge = el.renderAttribute("max-age")
?.let(Duration::parse)
?: Duration.ofDays(1)
val maxSize = el.renderAttribute("max-size")
?.let(String::toInt)
?: 0x100000
val compressionMode = el.renderAttribute("compression-mode")
?.let {
when (it) {
"gzip" -> CompressionMode.GZIP
"zip" -> CompressionMode.ZIP
else -> CompressionMode.ZIP
}
}
?: CompressionMode.ZIP
val digestAlgorithm = el.renderAttribute("digest")
for (child in el.asIterable()) {
when (child.nodeName) {
"server" -> {
val host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
val port = child.renderAttribute("port")?.toInt() ?: throw ConfigurationException("port attribute is required")
servers.add(HostAndPort(host, port))
}
}
}
return MemcachedCacheConfiguration(
servers,
maxAge,
maxSize,
digestAlgorithm,
compressionMode,
)
}
override fun serialize(doc: Document, cache: MemcachedCacheConfiguration) = cache.run {
val result = doc.createElement("cache")
Xml.of(doc, result) {
attr("xmlns:${xmlNamespacePrefix}", xmlNamespace, namespaceURI = "http://www.w3.org/2000/xmlns/")
attr("xs:type", "${xmlNamespacePrefix}:$xmlType", GBCS.XML_SCHEMA_NAMESPACE_URI)
for (server in servers) {
node("server") {
attr("host", server.host)
attr("port", server.port.toString())
}
}
attr("max-age", maxAge.toString())
attr("max-size", maxSize.toString())
digestAlgorithm?.let { digestAlgorithm ->
attr("digest", digestAlgorithm)
}
attr(
"compression-mode", when (compressionMode) {
CompressionMode.GZIP -> "gzip"
CompressionMode.ZIP -> "zip"
}
)
}
result
}
}

View File

@@ -0,0 +1 @@
net.woggioni.gbcs.server.memcached.MemcachedCacheProvider

View File

@@ -1,35 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.rbcs.server.memcache" <xs:schema targetNamespace="urn:net.woggioni.gbcs.server.memcached"
xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache" xmlns:gbcs-memcached="urn:net.woggioni.gbcs.server.memcached"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema"> xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import schemaLocation="jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd" namespace="urn:net.woggioni.rbcs.server"/> <xs:import schemaLocation="jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd" namespace="urn:net.woggioni.gbcs.server"/>
<xs:complexType name="memcacheServerType"> <xs:complexType name="memcachedServerType">
<xs:attribute name="host" type="xs:token" use="required"/> <xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:positiveInteger" use="required"/> <xs:attribute name="port" type="xs:positiveInteger" use="required"/>
<xs:attribute name="connection-timeout" type="xs:duration"/>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="1"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="memcacheCacheType"> <xs:complexType name="memcachedCacheType">
<xs:complexContent> <xs:complexContent>
<xs:extension base="rbcs:cacheType"> <xs:extension base="gbcs:cacheType">
<xs:sequence maxOccurs="unbounded"> <xs:sequence maxOccurs="unbounded">
<xs:element name="server" type="rbcs-memcache:memcacheServerType"/> <xs:element name="server" type="gbcs-memcached:memcachedServerType"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="max-age" type="xs:duration" default="P1D"/> <xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="max-size" type="xs:unsignedInt" default="1048576"/> <xs:attribute name="max-size" type="xs:unsignedInt" default="1048576"/>
<xs:attribute name="digest" type="xs:token" /> <xs:attribute name="digest" type="xs:token" />
<xs:attribute name="compression-mode" type="rbcs-memcache:compressionType"/> <xs:attribute name="compression-mode" type="gbcs-memcached:compressionType" default="zip"/>
</xs:extension> </xs:extension>
</xs:complexContent> </xs:complexContent>
</xs:complexType> </xs:complexType>
<xs:simpleType name="compressionType"> <xs:simpleType name="compressionType">
<xs:restriction base="xs:token"> <xs:restriction base="xs:token">
<xs:enumeration value="deflate"/> <xs:enumeration value="zip"/>
<xs:enumeration value="gzip"/> <xs:enumeration value="gzip"/>
</xs:restriction> </xs:restriction>
</xs:simpleType> </xs:simpleType>

View File

@@ -9,12 +9,9 @@ dependencies {
implementation catalog.jwo implementation catalog.jwo
implementation catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.netty.codec.http implementation catalog.netty.codec.http
implementation catalog.netty.handler
implementation catalog.netty.buffer
implementation catalog.netty.transport
api project(':rbcs-common') api project(':gbcs-common')
api project(':rbcs-api') api project(':gbcs-api')
// runtimeOnly catalog.slf4j.jdk14 // runtimeOnly catalog.slf4j.jdk14
testRuntimeOnly catalog.logback.classic testRuntimeOnly catalog.logback.classic
@@ -22,7 +19,7 @@ dependencies {
testImplementation catalog.bcprov.jdk18on testImplementation catalog.bcprov.jdk18on
testImplementation catalog.bcpkix.jdk18on testImplementation catalog.bcpkix.jdk18on
testRuntimeOnly project(":rbcs-server-memcache") testRuntimeOnly project(":gbcs-server-memcached")
} }
test { test {
@@ -39,4 +36,3 @@ publishing {
} }

View File

@@ -1,8 +1,8 @@
import net.woggioni.rbcs.api.CacheProvider; import net.woggioni.gbcs.api.CacheProvider;
import net.woggioni.rbcs.server.cache.FileSystemCacheProvider; import net.woggioni.gbcs.server.cache.FileSystemCacheProvider;
import net.woggioni.rbcs.server.cache.InMemoryCacheProvider; import net.woggioni.gbcs.server.cache.InMemoryCacheProvider;
module net.woggioni.rbcs.server { module net.woggioni.gbcs.server {
requires java.sql; requires java.sql;
requires java.xml; requires java.xml;
requires java.logging; requires java.logging;
@@ -16,13 +16,13 @@ module net.woggioni.rbcs.server {
requires io.netty.codec; requires io.netty.codec;
requires org.slf4j; requires org.slf4j;
requires net.woggioni.jwo; requires net.woggioni.jwo;
requires net.woggioni.rbcs.common; requires net.woggioni.gbcs.common;
requires net.woggioni.rbcs.api; requires net.woggioni.gbcs.api;
exports net.woggioni.rbcs.server; exports net.woggioni.gbcs.server;
opens net.woggioni.rbcs.server; opens net.woggioni.gbcs.server;
opens net.woggioni.rbcs.server.schema; opens net.woggioni.gbcs.server.schema;
uses CacheProvider; uses CacheProvider;
provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider; provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider;

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server package net.woggioni.gbcs.server
import io.netty.bootstrap.ServerBootstrap import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
@@ -30,26 +30,26 @@ import io.netty.handler.timeout.IdleStateHandler
import io.netty.util.AttributeKey import io.netty.util.AttributeKey
import io.netty.util.concurrent.DefaultEventExecutorGroup import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.common.GBCS.toUrl
import net.woggioni.gbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.debug
import net.woggioni.gbcs.common.info
import net.woggioni.gbcs.server.auth.AbstractNettyHttpAuthenticator
import net.woggioni.gbcs.server.auth.Authorizer
import net.woggioni.gbcs.server.auth.ClientCertificateValidator
import net.woggioni.gbcs.server.auth.RoleAuthorizer
import net.woggioni.gbcs.server.configuration.Parser
import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.gbcs.server.exception.ExceptionHandler
import net.woggioni.gbcs.server.handler.ServerHandler
import net.woggioni.gbcs.server.throttling.ThrottlingHandler
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2 import net.woggioni.jwo.Tuple2
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import net.woggioni.rbcs.common.RBCS.toUrl
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.common.contextLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.info
import net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator
import net.woggioni.rbcs.server.auth.Authorizer
import net.woggioni.rbcs.server.auth.ClientCertificateValidator
import net.woggioni.rbcs.server.auth.RoleAuthorizer
import net.woggioni.rbcs.server.configuration.Parser
import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.exception.ExceptionHandler
import net.woggioni.rbcs.server.handler.ServerHandler
import net.woggioni.rbcs.server.throttling.ThrottlingHandler
import java.io.OutputStream import java.io.OutputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.file.Files import java.nio.file.Files
@@ -59,14 +59,13 @@ import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.Arrays import java.util.Arrays
import java.util.Base64 import java.util.Base64
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.regex.Matcher import java.util.regex.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.naming.ldap.LdapName import javax.naming.ldap.LdapName
import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLPeerUnverifiedException
class RemoteBuildCacheServer(private val cfg: Configuration) { class GradleBuildCacheServer(private val cfg: Configuration) {
private val log = contextLogger() private val log = contextLogger()
companion object { companion object {
@@ -74,7 +73,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user") val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user")
val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group") val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group")
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/rbcs/server/rbcs-default.xml".toUrl() } val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() }
private const val SSL_HANDLER_NAME = "sslHandler" private const val SSL_HANDLER_NAME = "sslHandler"
fun loadConfiguration(configurationFile: Path): Configuration { fun loadConfiguration(configurationFile: Path): Configuration {
@@ -129,8 +128,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
val clientCertificate = peerCertificates.first() as X509Certificate val clientCertificate = peerCertificates.first() as X509Certificate
val user = userExtractor?.extract(clientCertificate) val user = userExtractor?.extract(clientCertificate)
val group = groupExtractor?.extract(clientCertificate) val group = groupExtractor?.extract(clientCertificate)
val allGroups = val allGroups = ((user?.groups ?: emptySet()).asSequence() + sequenceOf(group).filterNotNull()).toSet()
((user?.groups ?: emptySet()).asSequence() + sequenceOf(group).filterNotNull()).toSet()
AuthenticationResult(user, allGroups) AuthenticationResult(user, allGroups)
} ?: anonymousUserGroups?.let{ AuthenticationResult(null, it) } } ?: anonymousUserGroups?.let{ AuthenticationResult(null, it) }
} catch (es: SSLPeerUnverifiedException) { } catch (es: SSLPeerUnverifiedException) {
@@ -193,7 +191,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
private class ServerInitializer( private class ServerInitializer(
private val cfg: Configuration, private val cfg: Configuration,
private val eventExecutorGroup: EventExecutorGroup private val eventExecutorGroup: EventExecutorGroup
) : ChannelInitializer<Channel>(), AutoCloseable { ) : ChannelInitializer<Channel>() {
companion object { companion object {
private fun createSslCtx(tls: Configuration.Tls): SslContext { private fun createSslCtx(tls: Configuration.Tls): SslContext {
@@ -247,11 +245,10 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
private val log = contextLogger() private val log = contextLogger()
private val cache = cfg.cache.materialize()
private val serverHandler = let { private val serverHandler = let {
val cacheImplementation = cfg.cache.materialize()
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/")) val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
ServerHandler(cache, prefix) ServerHandler(cacheImplementation, prefix)
} }
private val exceptionHandler = ExceptionHandler() private val exceptionHandler = ExceptionHandler()
@@ -347,15 +344,12 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
IdleState.READER_IDLE -> log.debug { IdleState.READER_IDLE -> log.debug {
"Read timeout reached on channel ${ch.id().asShortText()}, closing the connection" "Read timeout reached on channel ${ch.id().asShortText()}, closing the connection"
} }
IdleState.WRITER_IDLE -> log.debug { IdleState.WRITER_IDLE -> log.debug {
"Write timeout reached on channel ${ch.id().asShortText()}, closing the connection" "Write timeout reached on channel ${ch.id().asShortText()}, closing the connection"
} }
IdleState.ALL_IDLE -> log.debug { IdleState.ALL_IDLE -> log.debug {
"Idle timeout reached on channel ${ch.id().asShortText()}, closing the connection" "Idle timeout reached on channel ${ch.id().asShortText()}, closing the connection"
} }
null -> throw IllegalStateException("This should never happen") null -> throw IllegalStateException("This should never happen")
} }
ctx.close() ctx.close()
@@ -376,54 +370,39 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
pipeline.addLast(eventExecutorGroup, serverHandler) pipeline.addLast(eventExecutorGroup, serverHandler)
pipeline.addLast(exceptionHandler) pipeline.addLast(exceptionHandler)
} }
override fun close() {
cache.close()
}
} }
class ServerHandle( class ServerHandle(
httpChannelFuture: ChannelFuture, httpChannelFuture: ChannelFuture,
private val executorGroups: Iterable<EventExecutorGroup>, private val executorGroups: Iterable<EventExecutorGroup>
private val serverInitializer: AutoCloseable
) : AutoCloseable { ) : AutoCloseable {
private val httpChannel: Channel = httpChannelFuture.channel() private val httpChannel: Channel = httpChannelFuture.channel()
private val closeFuture: ChannelFuture = httpChannel.closeFuture() private val closeFuture: ChannelFuture = httpChannel.closeFuture()
private val log = contextLogger() private val log = contextLogger()
fun shutdown(): Future<Void> { fun shutdown(): ChannelFuture {
return httpChannel.close() return httpChannel.close()
} }
override fun close() { override fun close() {
try { try {
closeFuture.sync() closeFuture.sync()
} catch (ex: Throwable) { } finally {
log.error(ex.message, ex)
}
try {
serverInitializer.close()
} catch (ex: Throwable) {
log.error(ex.message, ex)
}
executorGroups.forEach { executorGroups.forEach {
try {
it.shutdownGracefully().sync() it.shutdownGracefully().sync()
} catch (ex: Throwable) {
log.error(ex.message, ex)
} }
} }
log.info { log.info {
"RemoteBuildCacheServer has been gracefully shut down" "GradleBuildCacheServer has been gracefully shut down"
} }
} }
} }
fun run(): ServerHandle { fun run(): ServerHandle {
// Create the multithreaded event loops for the server // Create the multithreaded event loops for the server
val bossGroup = NioEventLoopGroup(1) val bossGroup = NioEventLoopGroup(0)
val serverSocketChannel = NioServerSocketChannel::class.java val serverSocketChannel = NioServerSocketChannel::class.java
val workerGroup = NioEventLoopGroup(0) val workerGroup = bossGroup
val eventExecutorGroup = run { val eventExecutorGroup = run {
val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) { val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) {
Thread.ofVirtual().factory() Thread.ofVirtual().factory()
@@ -432,12 +411,11 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
} }
DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory) DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory)
} }
val serverInitializer = ServerInitializer(cfg, eventExecutorGroup)
val bootstrap = ServerBootstrap().apply { val bootstrap = ServerBootstrap().apply {
// Configure the server // Configure the server
group(bossGroup, workerGroup) group(bossGroup, workerGroup)
channel(serverSocketChannel) channel(serverSocketChannel)
childHandler(serverInitializer) childHandler(ServerInitializer(cfg, eventExecutorGroup))
option(ChannelOption.SO_BACKLOG, cfg.incomingConnectionsBacklogSize) option(ChannelOption.SO_BACKLOG, cfg.incomingConnectionsBacklogSize)
childOption(ChannelOption.SO_KEEPALIVE, true) childOption(ChannelOption.SO_KEEPALIVE, true)
} }
@@ -447,8 +425,8 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
val bindAddress = InetSocketAddress(cfg.host, cfg.port) val bindAddress = InetSocketAddress(cfg.host, cfg.port)
val httpChannel = bootstrap.bind(bindAddress).sync() val httpChannel = bootstrap.bind(bindAddress).sync()
log.info { log.info {
"RemoteBuildCacheServer is listening on ${cfg.host}:${cfg.port}" "GradleBuildCacheServer is listening on ${cfg.host}:${cfg.port}"
} }
return ServerHandle(httpChannel, setOf(bossGroup, workerGroup, eventExecutorGroup), serverInitializer) return ServerHandle(httpChannel, setOf(bossGroup, workerGroup, eventExecutorGroup))
} }
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server package net.woggioni.gbcs.server
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import org.slf4j.Logger import org.slf4j.Logger

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.auth package net.woggioni.gbcs.server.auth
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener
@@ -11,10 +11,10 @@ import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.util.ReferenceCountUtil import io.netty.util.ReferenceCountUtil
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.api.Configuration.Group import net.woggioni.gbcs.api.Configuration.Group
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.rbcs.server.RemoteBuildCacheServer import net.woggioni.gbcs.server.GradleBuildCacheServer
abstract class AbstractNettyHttpAuthenticator(private val authorizer: Authorizer) : ChannelInboundHandlerAdapter() { abstract class AbstractNettyHttpAuthenticator(private val authorizer: Authorizer) : ChannelInboundHandlerAdapter() {
@@ -40,8 +40,8 @@ abstract class AbstractNettyHttpAuthenticator(private val authorizer: Authorizer
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is HttpRequest) { if (msg is HttpRequest) {
val result = authenticate(ctx, msg) ?: return authenticationFailure(ctx, msg) val result = authenticate(ctx, msg) ?: return authenticationFailure(ctx, msg)
ctx.channel().attr(RemoteBuildCacheServer.userAttribute).set(result.user) ctx.channel().attr(GradleBuildCacheServer.userAttribute).set(result.user)
ctx.channel().attr(RemoteBuildCacheServer.groupAttribute).set(result.groups) ctx.channel().attr(GradleBuildCacheServer.groupAttribute).set(result.groups)
val roles = ( val roles = (
(result.user?.let { user -> (result.user?.let { user ->

View File

@@ -1,7 +1,7 @@
package net.woggioni.rbcs.server.auth package net.woggioni.gbcs.server.auth
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
fun interface Authorizer { fun interface Authorizer {
fun authorize(roles : Set<Role>, request: HttpRequest) : Boolean fun authorize(roles : Set<Role>, request: HttpRequest) : Boolean

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.auth package net.woggioni.gbcs.server.auth
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.channel.ChannelInboundHandlerAdapter

View File

@@ -1,8 +1,8 @@
package net.woggioni.rbcs.server.auth package net.woggioni.gbcs.server.auth
import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
class RoleAuthorizer : Authorizer { class RoleAuthorizer : Authorizer {

View File

@@ -0,0 +1,21 @@
package net.woggioni.gbcs.server.cache
import net.woggioni.jwo.JWO
import java.security.MessageDigest
object CacheUtils {
fun digest(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): ByteArray {
md.update(data)
return md.digest()
}
fun digestString(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): String {
return JWO.bytesToHex(digest(data, md))
}
}

View File

@@ -1,11 +1,9 @@
package net.woggioni.rbcs.server.cache package net.woggioni.gbcs.server.cache
import io.netty.buffer.ByteBuf import net.woggioni.gbcs.api.Cache
import net.woggioni.jwo.JWO import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.api.Cache import net.woggioni.gbcs.server.cache.CacheUtils.digestString
import net.woggioni.rbcs.common.ByteBufInputStream import net.woggioni.jwo.LockFile
import net.woggioni.rbcs.common.RBCS.digestString
import net.woggioni.rbcs.common.contextLogger
import java.nio.channels.Channels import java.nio.channels.Channels
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.file.Files import java.nio.file.Files
@@ -16,7 +14,7 @@ import java.nio.file.attribute.BasicFileAttributes
import java.security.MessageDigest import java.security.MessageDigest
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicReference
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater import java.util.zip.Inflater
@@ -30,19 +28,13 @@ class FileSystemCache(
val compressionLevel: Int val compressionLevel: Int
) : Cache { ) : Cache {
private companion object {
@JvmStatic
private val log = contextLogger() private val log = contextLogger()
}
init { init {
Files.createDirectories(root) Files.createDirectories(root)
} }
@Volatile private var nextGc = AtomicReference(Instant.now().plus(maxAge))
private var running = true
private var nextGc = Instant.now()
override fun get(key: String) = (digestAlgorithm override fun get(key: String) = (digestAlgorithm
?.let(MessageDigest::getInstance) ?.let(MessageDigest::getInstance)
@@ -68,12 +60,12 @@ class FileSystemCache(
FileChannel.open(file, StandardOpenOption.READ) FileChannel.open(file, StandardOpenOption.READ)
} }
} }
}.let { }.also {
CompletableFuture.completedFuture(it) gc()
} }
} }
override fun put(key: String, content: ByteBuf): CompletableFuture<Void> { override fun put(key: String, content: ByteArray) {
(digestAlgorithm (digestAlgorithm
?.let(MessageDigest::getInstance) ?.let(MessageDigest::getInstance)
?.let { md -> ?.let { md ->
@@ -90,58 +82,39 @@ class FileSystemCache(
it it
} }
}.use { }.use {
JWO.copy(ByteBufInputStream(content), it) it.write(content)
} }
Files.move(tmpFile, file, StandardCopyOption.ATOMIC_MOVE) Files.move(tmpFile, file, StandardCopyOption.ATOMIC_MOVE)
} catch (t: Throwable) { } catch (t: Throwable) {
Files.delete(tmpFile) Files.delete(tmpFile)
throw t throw t
} }
} }.also {
return CompletableFuture.completedFuture(null)
}
private val garbageCollector = Thread.ofVirtual().name("file-system-cache-gc").start {
while (running) {
gc() gc()
} }
} }
private fun gc() { private fun gc() {
val now = Instant.now() val now = Instant.now()
if (nextGc < now) { val oldValue = nextGc.getAndSet(now.plus(maxAge))
val oldestEntry = actualGc(now) if (oldValue < now) {
nextGc = (oldestEntry ?: now).plus(maxAge) actualGc(now)
} }
Thread.sleep(minOf(Duration.between(now, nextGc), Duration.ofSeconds(1)))
} }
/** @Synchronized
* Returns the creation timestamp of the oldest cache entry (if any) private fun actualGc(now: Instant) {
*/ Files.list(root).filter {
private fun actualGc(now: Instant) : Instant? {
var result :Instant? = null
Files.list(root)
.filter { path ->
JWO.splitExtension(path)
.map { it._2 }
.map { it != ".tmp" }
.orElse(true)
}
.filter {
val creationTimeStamp = Files.readAttributes(it, BasicFileAttributes::class.java) val creationTimeStamp = Files.readAttributes(it, BasicFileAttributes::class.java)
.creationTime() .creationTime()
.toInstant() .toInstant()
if(result == null || creationTimeStamp < result) {
result = creationTimeStamp
}
now > creationTimeStamp.plus(maxAge) now > creationTimeStamp.plus(maxAge)
}.forEach(Files::delete) }.forEach { file ->
return result LockFile.acquire(file, false).use {
Files.delete(file)
}
}
} }
override fun close() { override fun close() {}
running = false
garbageCollector.join()
}
} }

View File

@@ -1,7 +1,7 @@
package net.woggioni.rbcs.server.cache package net.woggioni.gbcs.server.cache
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS import net.woggioni.gbcs.common.GBCS
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
@@ -14,14 +14,14 @@ data class FileSystemCacheConfiguration(
val compressionLevel: Int, val compressionLevel: Int,
) : Configuration.Cache { ) : Configuration.Cache {
override fun materialize() = FileSystemCache( override fun materialize() = FileSystemCache(
root ?: Application.builder("rbcs").build().computeCacheDirectory(), root ?: Application.builder("gbcs").build().computeCacheDirectory(),
maxAge, maxAge,
digestAlgorithm, digestAlgorithm,
compressionEnabled, compressionEnabled,
compressionLevel compressionLevel
) )
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI override fun getNamespaceURI() = GBCS.GBCS_NAMESPACE_URI
override fun getTypeName() = "fileSystemCacheType" override fun getTypeName() = "fileSystemCacheType"
} }

View File

@@ -1,9 +1,9 @@
package net.woggioni.rbcs.server.cache package net.woggioni.gbcs.server.cache
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.rbcs.common.RBCS import net.woggioni.gbcs.common.GBCS
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import java.nio.file.Path import java.nio.file.Path
@@ -12,11 +12,11 @@ import java.util.zip.Deflater
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> { class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd" override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/server/schema/gbcs.xsd"
override fun getXmlType() = "fileSystemCacheType" override fun getXmlType() = "fileSystemCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.rbcs.server" override fun getXmlNamespace() = "urn:net.woggioni.gbcs.server"
override fun deserialize(el: Element): FileSystemCacheConfiguration { override fun deserialize(el: Element): FileSystemCacheConfiguration {
val path = el.renderAttribute("path") val path = el.renderAttribute("path")
@@ -44,8 +44,8 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
override fun serialize(doc: Document, cache : FileSystemCacheConfiguration) = cache.run { override fun serialize(doc: Document, cache : FileSystemCacheConfiguration) = cache.run {
val result = doc.createElement("cache") val result = doc.createElement("cache")
Xml.of(doc, result) { Xml.of(doc, result) {
val prefix = doc.lookupPrefix(RBCS.RBCS_NAMESPACE_URI) val prefix = doc.lookupPrefix(GBCS.GBCS_NAMESPACE_URI)
attr("xs:type", "${prefix}:fileSystemCacheType", RBCS.XML_SCHEMA_NAMESPACE_URI) attr("xs:type", "${prefix}:fileSystemCacheType", GBCS.XML_SCHEMA_NAMESPACE_URI)
attr("path", root.toString()) attr("path", root.toString())
attr("max-age", maxAge.toString()) attr("max-age", maxAge.toString())
digestAlgorithm?.let { digestAlgorithm -> digestAlgorithm?.let { digestAlgorithm ->

View File

@@ -0,0 +1,106 @@
package net.woggioni.gbcs.server.cache
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.server.cache.CacheUtils.digestString
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
class InMemoryCache(
val maxAge: Duration,
val digestAlgorithm: String?,
val compressionEnabled: Boolean,
val compressionLevel: Int
) : Cache {
private val map = ConcurrentHashMap<String, MapValue>()
private class MapValue(val rc: AtomicInteger, val payload : AtomicReference<ByteArray>)
private class RemovalQueueElement(val key: String, val expiry : Instant) : Comparable<RemovalQueueElement> {
override fun compareTo(other: RemovalQueueElement)= expiry.compareTo(other.expiry)
}
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>()
private var running = true
private val garbageCollector = Thread({
while(true) {
val el = removalQueue.take()
val now = Instant.now()
if(now > el.expiry) {
val value = map[el.key] ?: continue
val rc = value.rc.decrementAndGet()
if(rc == 0) {
map.remove(el.key)
}
} else {
removalQueue.put(el)
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
}
}
}).apply {
start()
}
override fun close() {
running = false
garbageCollector.join()
}
override fun get(key: String) =
(digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key
).let { digest ->
map[digest]
?.let(MapValue::payload)
?.let(AtomicReference<ByteArray>::get)
?.let { value ->
if (compressionEnabled) {
val inflater = Inflater()
Channels.newChannel(InflaterInputStream(ByteArrayInputStream(value), inflater))
} else {
Channels.newChannel(ByteArrayInputStream(value))
}
}
}
override fun put(key: String, content: ByteArray) {
(digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
val value = if (compressionEnabled) {
val deflater = Deflater(compressionLevel)
val baos = ByteArrayOutputStream()
DeflaterOutputStream(baos, deflater).use { stream ->
stream.write(content)
}
baos.toByteArray()
} else {
content
}
val mapValue = map.computeIfAbsent(digest) {
MapValue(AtomicInteger(0), AtomicReference())
}
mapValue.payload.set(value)
removalQueue.put(RemovalQueueElement(digest, Instant.now().plus(maxAge)))
}
}
}

View File

@@ -1,25 +1,23 @@
package net.woggioni.rbcs.server.cache package net.woggioni.gbcs.server.cache
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS import net.woggioni.gbcs.common.GBCS
import java.time.Duration import java.time.Duration
data class InMemoryCacheConfiguration( data class InMemoryCacheConfiguration(
val maxAge: Duration, val maxAge: Duration,
val maxSize: Long,
val digestAlgorithm : String?, val digestAlgorithm : String?,
val compressionEnabled: Boolean, val compressionEnabled: Boolean,
val compressionLevel: Int, val compressionLevel: Int,
) : Configuration.Cache { ) : Configuration.Cache {
override fun materialize() = InMemoryCache( override fun materialize() = InMemoryCache(
maxAge, maxAge,
maxSize,
digestAlgorithm, digestAlgorithm,
compressionEnabled, compressionEnabled,
compressionLevel compressionLevel
) )
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI override fun getNamespaceURI() = GBCS.GBCS_NAMESPACE_URI
override fun getTypeName() = "inMemoryCacheType" override fun getTypeName() = "inMemoryCacheType"
} }

View File

@@ -1,29 +1,27 @@
package net.woggioni.rbcs.server.cache package net.woggioni.gbcs.server.cache
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.rbcs.common.RBCS import net.woggioni.gbcs.common.GBCS
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.util.zip.Deflater import java.util.zip.Deflater
class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> { class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd" override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/server/schema/gbcs.xsd"
override fun getXmlType() = "inMemoryCacheType" override fun getXmlType() = "inMemoryCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.rbcs.server" override fun getXmlNamespace() = "urn:net.woggioni.gbcs.server"
override fun deserialize(el: Element): InMemoryCacheConfiguration { override fun deserialize(el: Element): InMemoryCacheConfiguration {
val maxAge = el.renderAttribute("max-age") val maxAge = el.renderAttribute("max-age")
?.let(Duration::parse) ?.let(Duration::parse)
?: Duration.ofDays(1) ?: Duration.ofDays(1)
val maxSize = el.renderAttribute("max-size")
?.let(java.lang.Long::decode)
?: 0x1000000
val enableCompression = el.renderAttribute("enable-compression") val enableCompression = el.renderAttribute("enable-compression")
?.let(String::toBoolean) ?.let(String::toBoolean)
?: true ?: true
@@ -34,7 +32,6 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
return InMemoryCacheConfiguration( return InMemoryCacheConfiguration(
maxAge, maxAge,
maxSize,
digestAlgorithm, digestAlgorithm,
enableCompression, enableCompression,
compressionLevel compressionLevel
@@ -44,10 +41,9 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
override fun serialize(doc: Document, cache : InMemoryCacheConfiguration) = cache.run { override fun serialize(doc: Document, cache : InMemoryCacheConfiguration) = cache.run {
val result = doc.createElement("cache") val result = doc.createElement("cache")
Xml.of(doc, result) { Xml.of(doc, result) {
val prefix = doc.lookupPrefix(RBCS.RBCS_NAMESPACE_URI) val prefix = doc.lookupPrefix(GBCS.GBCS_NAMESPACE_URI)
attr("xs:type", "${prefix}:inMemoryCacheType", RBCS.XML_SCHEMA_NAMESPACE_URI) attr("xs:type", "${prefix}:inMemoryCacheType", GBCS.XML_SCHEMA_NAMESPACE_URI)
attr("max-age", maxAge.toString()) attr("max-age", maxAge.toString())
attr("max-size", maxSize.toString())
digestAlgorithm?.let { digestAlgorithm -> digestAlgorithm?.let { digestAlgorithm ->
attr("digest", digestAlgorithm) attr("digest", digestAlgorithm)
} }

View File

@@ -1,7 +1,7 @@
package net.woggioni.rbcs.server.configuration package net.woggioni.gbcs.server.configuration
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import java.util.ServiceLoader import java.util.ServiceLoader
object CacheSerializers { object CacheSerializers {

View File

@@ -1,20 +1,20 @@
package net.woggioni.rbcs.server.configuration package net.woggioni.gbcs.server.configuration
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.api.Configuration.Authentication import net.woggioni.gbcs.api.Configuration.Authentication
import net.woggioni.rbcs.api.Configuration.BasicAuthentication import net.woggioni.gbcs.api.Configuration.BasicAuthentication
import net.woggioni.rbcs.api.Configuration.Cache import net.woggioni.gbcs.api.Configuration.Cache
import net.woggioni.rbcs.api.Configuration.ClientCertificateAuthentication import net.woggioni.gbcs.api.Configuration.ClientCertificateAuthentication
import net.woggioni.rbcs.api.Configuration.Group import net.woggioni.gbcs.api.Configuration.Group
import net.woggioni.rbcs.api.Configuration.KeyStore import net.woggioni.gbcs.api.Configuration.KeyStore
import net.woggioni.rbcs.api.Configuration.Tls import net.woggioni.gbcs.api.Configuration.Tls
import net.woggioni.rbcs.api.Configuration.TlsCertificateExtractor import net.woggioni.gbcs.api.Configuration.TlsCertificateExtractor
import net.woggioni.rbcs.api.Configuration.TrustStore import net.woggioni.gbcs.api.Configuration.TrustStore
import net.woggioni.rbcs.api.Configuration.User import net.woggioni.gbcs.api.Configuration.User
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.rbcs.api.exception.ConfigurationException import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.common.Xml.Companion.asIterable import net.woggioni.gbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.TypeInfo import org.w3c.dom.TypeInfo
@@ -265,8 +265,7 @@ object Parser {
}.map { el -> }.map { el ->
val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required") val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required")
var roles = emptySet<Role>() var roles = emptySet<Role>()
var userQuota: Configuration.Quota? = null var quota: Configuration.Quota? = null
var groupQuota: Configuration.Quota? = null
for (child in el.asIterable()) { for (child in el.asIterable()) {
when (child.localName) { when (child.localName) {
"users" -> { "users" -> {
@@ -280,15 +279,12 @@ object Parser {
"roles" -> { "roles" -> {
roles = parseRoles(child) roles = parseRoles(child)
} }
"group-quota" -> { "quota" -> {
userQuota = parseQuota(child) quota = parseQuota(child)
}
"user-quota" -> {
groupQuota = parseQuota(child)
} }
} }
} }
groupName to Group(groupName, roles, userQuota, groupQuota) groupName to Group(groupName, roles, quota)
}.toMap() }.toMap()
val users = knownUsersMap.map { (name, user) -> val users = knownUsersMap.map { (name, user) ->
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet(), user.quota) name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet(), user.quota)

View File

@@ -1,28 +1,22 @@
package net.woggioni.rbcs.server.configuration package net.woggioni.gbcs.server.configuration
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS import net.woggioni.gbcs.common.GBCS
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import org.w3c.dom.Document import org.w3c.dom.Document
object Serializer { object Serializer {
private fun Xml.serializeQuota(quota : Configuration.Quota) {
attr("calls", quota.calls.toString())
attr("period", quota.period.toString())
attr("max-available-calls", quota.maxAvailableCalls.toString())
attr("initial-available-calls", quota.initialAvailableCalls.toString())
}
fun serialize(conf : Configuration) : Document { fun serialize(conf : Configuration) : Document {
val schemaLocations = CacheSerializers.index.values.asSequence().map { val schemaLocations = CacheSerializers.index.values.asSequence().map {
it.xmlNamespace to it.xmlSchemaLocation it.xmlNamespace to it.xmlSchemaLocation
}.toMap() }.toMap()
return Xml.of(RBCS.RBCS_NAMESPACE_URI, RBCS.RBCS_PREFIX + ":server") { return Xml.of(GBCS.GBCS_NAMESPACE_URI, GBCS.GBCS_PREFIX + ":server") {
// attr("xmlns:xs", GradleBuildCacheServer.XML_SCHEMA_NAMESPACE_URI) // attr("xmlns:xs", GradleBuildCacheServer.XML_SCHEMA_NAMESPACE_URI)
val value = schemaLocations.asSequence().map { (k, v) -> "$k $v" }.joinToString(" ") val value = schemaLocations.asSequence().map { (k, v) -> "$k $v" }.joinToString(" ")
attr("xs:schemaLocation", value , namespaceURI = RBCS.XML_SCHEMA_NAMESPACE_URI) attr("xs:schemaLocation", value , namespaceURI = GBCS.XML_SCHEMA_NAMESPACE_URI)
conf.serverPath conf.serverPath
?.takeIf(String::isNotEmpty) ?.takeIf(String::isNotEmpty)
@@ -62,7 +56,10 @@ object Serializer {
} }
user.quota?.let { quota -> user.quota?.let { quota ->
node("quota") { node("quota") {
serializeQuota(quota) attr("calls", quota.calls.toString())
attr("period", quota.period.toString())
attr("max-available-calls", quota.maxAvailableCalls.toString())
attr("initial-available-calls", quota.initialAvailableCalls.toString())
} }
} }
} }
@@ -73,7 +70,10 @@ object Serializer {
anonymousUser.quota?.let { quota -> anonymousUser.quota?.let { quota ->
node("anonymous") { node("anonymous") {
node("quota") { node("quota") {
serializeQuota(quota) attr("calls", quota.calls.toString())
attr("period", quota.period.toString())
attr("max-available-calls", quota.maxAvailableCalls.toString())
attr("initial-available-calls", quota.initialAvailableCalls.toString())
} }
} }
} }
@@ -113,14 +113,12 @@ object Serializer {
} }
} }
} }
group.userQuota?.let { quota -> group.quota?.let { quota ->
node("user-quota") { node("quota") {
serializeQuota(quota) attr("calls", quota.calls.toString())
} attr("period", quota.period.toString())
} attr("max-available-calls", quota.maxAvailableCalls.toString())
group.groupQuota?.let { quota -> attr("initial-available-calls", quota.initialAvailableCalls.toString())
node("group-quota") {
serializeQuota(quota)
} }
} }
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.exception package net.woggioni.gbcs.server.exception
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelDuplexHandler
@@ -13,12 +13,10 @@ import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.timeout.ReadTimeoutException import io.netty.handler.timeout.ReadTimeoutException
import io.netty.handler.timeout.WriteTimeoutException import io.netty.handler.timeout.WriteTimeoutException
import net.woggioni.rbcs.api.exception.CacheException import net.woggioni.gbcs.api.exception.CacheException
import net.woggioni.rbcs.api.exception.ContentTooLargeException import net.woggioni.gbcs.api.exception.ContentTooLargeException
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.common.debug import net.woggioni.gbcs.common.debug
import java.net.SocketException
import javax.net.ssl.SSLException
import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLPeerUnverifiedException
@ChannelHandler.Sharable @ChannelHandler.Sharable
@@ -52,12 +50,7 @@ class ExceptionHandler : ChannelDuplexHandler() {
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
when (cause) { when (cause) {
is DecoderException -> { is DecoderException -> {
log.debug(cause.message, cause) log.error(cause.message, cause)
ctx.close()
}
is SocketException -> {
log.debug(cause.message, cause)
ctx.close() ctx.close()
} }
@@ -66,16 +59,10 @@ class ExceptionHandler : ChannelDuplexHandler() {
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE) .addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
} }
is SSLException -> {
log.debug(cause.message, cause)
ctx.close()
}
is ContentTooLargeException -> { is ContentTooLargeException -> {
ctx.writeAndFlush(TOO_BIG.retainedDuplicate()) ctx.writeAndFlush(TOO_BIG.retainedDuplicate())
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE) .addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
} }
is ReadTimeoutException -> { is ReadTimeoutException -> {
log.debug { log.debug {
val channelId = ctx.channel().id().asShortText() val channelId = ctx.channel().id().asShortText()
@@ -83,7 +70,6 @@ class ExceptionHandler : ChannelDuplexHandler() {
} }
ctx.close() ctx.close()
} }
is WriteTimeoutException -> { is WriteTimeoutException -> {
log.debug { log.debug {
val channelId = ctx.channel().id().asShortText() val channelId = ctx.channel().id().asShortText()
@@ -91,13 +77,11 @@ class ExceptionHandler : ChannelDuplexHandler() {
} }
ctx.close() ctx.close()
} }
is CacheException -> { is CacheException -> {
log.error(cause.message, cause) log.error(cause.message, cause)
ctx.writeAndFlush(NOT_AVAILABLE.retainedDuplicate()) ctx.writeAndFlush(NOT_AVAILABLE.retainedDuplicate())
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE) .addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
} }
else -> { else -> {
log.error(cause.message, cause) log.error(cause.message, cause)
ctx.writeAndFlush(SERVER_ERROR.retainedDuplicate()) ctx.writeAndFlush(SERVER_ERROR.retainedDuplicate())

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.handler package net.woggioni.gbcs.server.handler
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener
@@ -16,10 +16,11 @@ import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpUtil import io.netty.handler.codec.http.HttpUtil
import io.netty.handler.codec.http.LastHttpContent import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.stream.ChunkedNioStream import io.netty.handler.stream.ChunkedNioStream
import net.woggioni.rbcs.api.Cache import net.woggioni.gbcs.api.Cache
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.api.exception.CacheException
import net.woggioni.rbcs.server.debug import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.server.warn import net.woggioni.gbcs.server.debug
import net.woggioni.gbcs.server.warn
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.file.Path import java.nio.file.Path
@@ -42,8 +43,11 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
return return
} }
if (serverPrefix == prefix) { if (serverPrefix == prefix) {
cache.get(key).thenApply { channel -> try {
if(channel != null) { cache.get(key)
} catch(ex : Throwable) {
throw CacheException("Error accessing the cache backend", ex)
}?.let { channel ->
log.debug(ctx) { log.debug(ctx) {
"Cache hit for key '$key'" "Cache hit for key '$key'"
} }
@@ -59,29 +63,22 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
ctx.write(response) ctx.write(response)
when (channel) { when (channel) {
is FileChannel -> { is FileChannel -> {
val content = DefaultFileRegion(channel, 0, channel.size())
if (keepAlive) { if (keepAlive) {
ctx.write(content) ctx.write(DefaultFileRegion(channel, 0, channel.size()))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate()) ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate())
} else { } else {
ctx.writeAndFlush(content) ctx.writeAndFlush(DefaultFileRegion(channel, 0, channel.size()))
.addListener(ChannelFutureListener.CLOSE) .addListener(ChannelFutureListener.CLOSE)
} }
} }
else -> { else -> {
val content = ChunkedNioStream(channel) ctx.write(ChunkedNioStream(channel)).addListener { evt ->
if (keepAlive) { channel.close()
ctx.write(content).addListener {
content.close()
} }
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate()) ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate())
} else {
ctx.writeAndFlush(content)
.addListener(ChannelFutureListener.CLOSE)
} }
} }
} } ?: let {
} else {
log.debug(ctx) { log.debug(ctx) {
"Cache miss for key '$key'" "Cache miss for key '$key'"
} }
@@ -89,7 +86,6 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0 response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response) ctx.writeAndFlush(response)
} }
}.whenComplete { _, ex -> ex?.let(ctx::fireExceptionCaught) }
} else { } else {
log.warn(ctx) { log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'" "Got request for unhandled path '${msg.uri()}'"
@@ -107,16 +103,26 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
log.debug(ctx) { log.debug(ctx) {
"Added value for key '$key' to build cache" "Added value for key '$key' to build cache"
} }
cache.put(key, msg.content()).thenRun { val bodyBytes = msg.content().run {
if (isDirect) {
ByteArray(readableBytes()).also {
readBytes(it)
}
} else {
array()
}
}
try {
cache.put(key, bodyBytes)
} catch(ex : Throwable) {
throw CacheException("Error accessing the cache backend", ex)
}
val response = DefaultFullHttpResponse( val response = DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.CREATED, msg.protocolVersion(), HttpResponseStatus.CREATED,
Unpooled.copiedBuffer(key.toByteArray()) Unpooled.copiedBuffer(key.toByteArray())
) )
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes() response.headers()[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes()
ctx.writeAndFlush(response) ctx.writeAndFlush(response)
}.whenComplete { _, ex ->
ctx.fireExceptionCaught(ex)
}
} else { } else {
log.warn(ctx) { log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'" "Got request for unhandled path '${msg.uri()}'"

View File

@@ -1,6 +1,6 @@
package net.woggioni.rbcs.server.throttling package net.woggioni.gbcs.server.throttling
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.jwo.Bucket import net.woggioni.jwo.Bucket
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.Arrays import java.util.Arrays
@@ -8,7 +8,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.function.Function import java.util.function.Function
class BucketManager private constructor( class BucketManager private constructor(
private val bucketsByUser: Map<Configuration.User, List<Bucket>> = HashMap(), private val bucketsByUser: Map<Configuration.User, Bucket> = HashMap(),
private val bucketsByGroup: Map<Configuration.Group, Bucket> = HashMap(), private val bucketsByGroup: Map<Configuration.Group, Bucket> = HashMap(),
loader: Function<InetSocketAddress, Bucket>? loader: Function<InetSocketAddress, Bucket>?
) { ) {
@@ -43,27 +43,22 @@ class BucketManager private constructor(
companion object { companion object {
fun from(cfg : Configuration) : BucketManager { fun from(cfg : Configuration) : BucketManager {
val bucketsByUser = cfg.users.values.asSequence().map { user -> val bucketsByUser = cfg.users.values.asSequence().filter {
val buckets = ( it.quota != null
user.quota }.map { user ->
?.let { quota -> val quota = user.quota
sequenceOf(quota) val bucket = Bucket.local(
} ?: user.groups.asSequence()
.mapNotNull(Configuration.Group::getUserQuota)
).map { quota ->
Bucket.local(
quota.maxAvailableCalls, quota.maxAvailableCalls,
quota.calls, quota.calls,
quota.period, quota.period,
quota.initialAvailableCalls quota.initialAvailableCalls
) )
}.toList() user to bucket
user to buckets
}.toMap() }.toMap()
val bucketsByGroup = cfg.groups.values.asSequence().filter { val bucketsByGroup = cfg.groups.values.asSequence().filter {
it.groupQuota != null it.quota != null
}.map { group -> }.map { group ->
val quota = group.groupQuota val quota = group.quota
val bucket = Bucket.local( val bucket = Bucket.local(
quota.maxAvailableCalls, quota.maxAvailableCalls,
quota.calls, quota.calls,

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.throttling package net.woggioni.gbcs.server.throttling
import io.netty.channel.ChannelHandler.Sharable import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
@@ -7,9 +7,9 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.common.contextLogger import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.server.RemoteBuildCacheServer import net.woggioni.gbcs.server.GradleBuildCacheServer
import net.woggioni.jwo.Bucket import net.woggioni.jwo.Bucket
import net.woggioni.jwo.LongMath import net.woggioni.jwo.LongMath
import java.net.InetSocketAddress import java.net.InetSocketAddress
@@ -19,13 +19,10 @@ import java.util.concurrent.TimeUnit
@Sharable @Sharable
class ThrottlingHandler(cfg: Configuration) : ChannelInboundHandlerAdapter() { class ThrottlingHandler(cfg: Configuration) :
ChannelInboundHandlerAdapter() {
private companion object {
@JvmStatic
private val log = contextLogger() private val log = contextLogger()
}
private val bucketManager = BucketManager.from(cfg) private val bucketManager = BucketManager.from(cfg)
private val connectionConfiguration = cfg.connection private val connectionConfiguration = cfg.connection
@@ -43,11 +40,11 @@ class ThrottlingHandler(cfg: Configuration) : ChannelInboundHandlerAdapter() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
val buckets = mutableListOf<Bucket>() val buckets = mutableListOf<Bucket>()
val user = ctx.channel().attr(RemoteBuildCacheServer.userAttribute).get() val user = ctx.channel().attr(GradleBuildCacheServer.userAttribute).get()
if (user != null) { if (user != null) {
bucketManager.getBucketByUser(user)?.let(buckets::addAll) bucketManager.getBucketByUser(user)?.let(buckets::add)
} }
val groups = ctx.channel().attr(RemoteBuildCacheServer.groupAttribute).get() ?: emptySet() val groups = ctx.channel().attr(GradleBuildCacheServer.groupAttribute).get() ?: emptySet()
if (groups.isNotEmpty()) { if (groups.isNotEmpty()) {
groups.forEach { group -> groups.forEach { group ->
bucketManager.getBucketByGroup(group)?.let(buckets::add) bucketManager.getBucketByGroup(group)?.let(buckets::add)
@@ -73,7 +70,8 @@ class ThrottlingHandler(cfg: Configuration) : ChannelInboundHandlerAdapter() {
} }
if(nextAttempt < 0) { if(nextAttempt < 0) {
super.channelRead(ctx, msg) super.channelRead(ctx, msg)
} else { return
}
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttempt, 100_000_000L) * 100L, ChronoUnit.MILLIS) val waitDuration = Duration.of(LongMath.ceilDiv(nextAttempt, 100_000_000L) * 100L, ChronoUnit.MILLIS)
if (delayResponse && waitDuration < waitThreshold) { if (delayResponse && waitDuration < waitThreshold) {
ctx.executor().schedule({ ctx.executor().schedule({
@@ -83,7 +81,6 @@ class ThrottlingHandler(cfg: Configuration) : ChannelInboundHandlerAdapter() {
sendThrottledResponse(ctx, waitDuration) sendThrottledResponse(ctx, waitDuration)
} }
} }
}
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration) { private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration) {
val response = DefaultFullHttpResponse( val response = DefaultFullHttpResponse(

View File

@@ -0,0 +1,2 @@
net.woggioni.gbcs.server.cache.FileSystemCacheProvider
net.woggioni.gbcs.server.cache.InMemoryCacheProvider

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server <gbcs:server
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="8080" incoming-connections-backlog-size="1024"/> <bind host="127.0.0.1" port="8080" incoming-connections-backlog-size="1024"/>
<connection <connection
max-request-size="67108864" max-request-size="67108864"
@@ -12,8 +12,8 @@
read-idle-timeout="PT60S" read-idle-timeout="PT60S"
write-idle-timeout="PT60S"/> write-idle-timeout="PT60S"/>
<event-executor use-virtual-threads="true"/> <event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authentication> <authentication>
<none/> <none/>
</authentication> </authentication>
</rbcs:server> </gbcs:server>

View File

@@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.rbcs.server" <xs:schema targetNamespace="urn:net.woggioni.gbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
elementFormDefault="unqualified"> elementFormDefault="unqualified">
<xs:element name="server" type="rbcs:serverType"/> <xs:element name="server" type="gbcs:serverType"/>
<xs:complexType name="serverType"> <xs:complexType name="serverType">
<xs:sequence minOccurs="0"> <xs:sequence minOccurs="0">
<xs:element name="bind" type="rbcs:bindType" maxOccurs="1"/> <xs:element name="bind" type="gbcs:bindType" maxOccurs="1"/>
<xs:element name="connection" type="rbcs:connectionType" minOccurs="0" maxOccurs="1"/> <xs:element name="connection" type="gbcs:connectionType" minOccurs="0" maxOccurs="1"/>
<xs:element name="event-executor" type="rbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/> <xs:element name="event-executor" type="gbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/>
<xs:element name="cache" type="rbcs:cacheType" maxOccurs="1"/> <xs:element name="cache" type="gbcs:cacheType" maxOccurs="1"/>
<xs:element name="authorization" type="rbcs:authorizationType" minOccurs="0"> <xs:element name="authorization" type="gbcs:authorizationType" minOccurs="0">
<xs:key name="userId"> <xs:key name="userId">
<xs:selector xpath="users/user"/> <xs:selector xpath="users/user"/>
<xs:field xpath="@name"/> <xs:field xpath="@name"/>
</xs:key> </xs:key>
<xs:keyref name="userRef" refer="rbcs:userId"> <xs:keyref name="userRef" refer="gbcs:userId">
<xs:selector xpath="groups/group/users/user"/> <xs:selector xpath="groups/group/users/user"/>
<xs:field xpath="@ref"/> <xs:field xpath="@ref"/>
</xs:keyref> </xs:keyref>
</xs:element> </xs:element>
<xs:element name="authentication" type="rbcs:authenticationType" minOccurs="0" maxOccurs="1"/> <xs:element name="authentication" type="gbcs:authenticationType" minOccurs="0" maxOccurs="1"/>
<xs:element name="tls" type="rbcs:tlsType" minOccurs="0" maxOccurs="1"/> <xs:element name="tls" type="gbcs:tlsType" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="path" type="xs:string" use="optional"/> <xs:attribute name="path" type="xs:string" use="optional"/>
</xs:complexType> </xs:complexType>
@@ -50,9 +50,8 @@
<xs:complexType name="inMemoryCacheType"> <xs:complexType name="inMemoryCacheType">
<xs:complexContent> <xs:complexContent>
<xs:extension base="rbcs:cacheType"> <xs:extension base="gbcs:cacheType">
<xs:attribute name="max-age" type="xs:duration" default="P1D"/> <xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="max-size" type="xs:token" default="0x1000000"/>
<xs:attribute name="digest" type="xs:token" default="MD5"/> <xs:attribute name="digest" type="xs:token" default="MD5"/>
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/> <xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
<xs:attribute name="compression-level" type="xs:byte" default="-1"/> <xs:attribute name="compression-level" type="xs:byte" default="-1"/>
@@ -62,7 +61,7 @@
<xs:complexType name="fileSystemCacheType"> <xs:complexType name="fileSystemCacheType">
<xs:complexContent> <xs:complexContent>
<xs:extension base="rbcs:cacheType"> <xs:extension base="gbcs:cacheType">
<xs:attribute name="path" type="xs:string" use="required"/> <xs:attribute name="path" type="xs:string" use="required"/>
<xs:attribute name="max-age" type="xs:duration" default="P1D"/> <xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="digest" type="xs:token" default="MD5"/> <xs:attribute name="digest" type="xs:token" default="MD5"/>
@@ -74,8 +73,8 @@
<xs:complexType name="tlsCertificateAuthorizationType"> <xs:complexType name="tlsCertificateAuthorizationType">
<xs:sequence> <xs:sequence>
<xs:element name="group-extractor" type="rbcs:X500NameExtractorType" minOccurs="0"/> <xs:element name="group-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
<xs:element name="user-extractor" type="rbcs:X500NameExtractorType" minOccurs="0"/> <xs:element name="user-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
@@ -86,8 +85,8 @@
<xs:complexType name="authorizationType"> <xs:complexType name="authorizationType">
<xs:all> <xs:all>
<xs:element name="users" type="rbcs:usersType"/> <xs:element name="users" type="gbcs:usersType"/>
<xs:element name="groups" type="rbcs:groupsType"> <xs:element name="groups" type="gbcs:groupsType">
<xs:unique name="groupKey"> <xs:unique name="groupKey">
<xs:selector xpath="group"/> <xs:selector xpath="group"/>
<xs:field xpath="@name"/> <xs:field xpath="@name"/>
@@ -99,7 +98,7 @@
<xs:complexType name="authenticationType"> <xs:complexType name="authenticationType">
<xs:choice> <xs:choice>
<xs:element name="basic"/> <xs:element name="basic"/>
<xs:element name="client-certificate" type="rbcs:tlsCertificateAuthorizationType"/> <xs:element name="client-certificate" type="gbcs:tlsCertificateAuthorizationType"/>
<xs:element name="none"/> <xs:element name="none"/>
</xs:choice> </xs:choice>
</xs:complexType> </xs:complexType>
@@ -113,13 +112,13 @@
<xs:complexType name="anonymousUserType"> <xs:complexType name="anonymousUserType">
<xs:sequence> <xs:sequence>
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/> <xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
<xs:complexType name="userType"> <xs:complexType name="userType">
<xs:sequence> <xs:sequence>
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/> <xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/> <xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="optional"/> <xs:attribute name="password" type="xs:string" use="optional"/>
@@ -127,28 +126,27 @@
<xs:complexType name="usersType"> <xs:complexType name="usersType">
<xs:sequence> <xs:sequence>
<xs:element name="user" type="rbcs:userType" minOccurs="0" maxOccurs="unbounded"/> <xs:element name="user" type="gbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="anonymous" type="rbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/> <xs:element name="anonymous" type="gbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
<xs:complexType name="groupsType"> <xs:complexType name="groupsType">
<xs:sequence> <xs:sequence>
<xs:element name="group" type="rbcs:groupType" maxOccurs="unbounded" minOccurs="0"/> <xs:element name="group" type="gbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
<xs:complexType name="groupType"> <xs:complexType name="groupType">
<xs:sequence> <xs:sequence>
<xs:element name="users" type="rbcs:userRefsType" maxOccurs="1" minOccurs="0"> <xs:element name="users" type="gbcs:userRefsType" maxOccurs="1" minOccurs="0">
<xs:unique name="userRefWriterKey"> <xs:unique name="userRefWriterKey">
<xs:selector xpath="user"/> <xs:selector xpath="user"/>
<xs:field xpath="@ref"/> <xs:field xpath="@ref"/>
</xs:unique> </xs:unique>
</xs:element> </xs:element>
<xs:element name="roles" type="rbcs:rolesType" maxOccurs="1" minOccurs="0"/> <xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/>
<xs:element name="user-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/> <xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
<xs:element name="group-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="name" type="xs:token"/> <xs:attribute name="name" type="xs:token"/>
</xs:complexType> </xs:complexType>
@@ -171,7 +169,7 @@
<xs:complexType name="userRefsType"> <xs:complexType name="userRefsType">
<xs:sequence> <xs:sequence>
<xs:element name="user" type="rbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/> <xs:element name="user" type="gbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/>
<xs:element name="anonymous" minOccurs="0" maxOccurs="1"/> <xs:element name="anonymous" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
@@ -182,8 +180,8 @@
<xs:complexType name="tlsType"> <xs:complexType name="tlsType">
<xs:all> <xs:all>
<xs:element name="keystore" type="rbcs:keyStoreType" /> <xs:element name="keystore" type="gbcs:keyStoreType" />
<xs:element name="truststore" type="rbcs:trustStoreType" minOccurs="0"/> <xs:element name="truststore" type="gbcs:trustStoreType" minOccurs="0"/>
</xs:all> </xs:all>
</xs:complexType> </xs:complexType>
@@ -203,7 +201,7 @@
<xs:complexType name="propertiesType"> <xs:complexType name="propertiesType">
<xs:sequence> <xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="rbcs:propertyType"/> <xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="gbcs:propertyType"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.test.utils; package net.woggioni.gbcs.server.test.utils;
import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500Name;

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.test.utils; package net.woggioni.gbcs.server.test.utils;
import net.woggioni.jwo.JWO; import net.woggioni.jwo.JWO;

View File

@@ -1,11 +1,11 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.test.utils.NetworkUtils import net.woggioni.gbcs.server.test.utils.NetworkUtils
import java.net.URI import java.net.URI
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@@ -23,9 +23,9 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
protected val random = Random(101325) protected val random = Random(101325)
protected val keyValuePair = newEntry(random) protected val keyValuePair = newEntry(random)
protected val serverPath = "rbcs" protected val serverPath = "gbcs"
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null, null) protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null)
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null, null) protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null)
abstract protected val users : List<Configuration.User> abstract protected val users : List<Configuration.User>

View File

@@ -1,7 +1,7 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.server.GradleBuildCacheServer
import net.woggioni.rbcs.server.RemoteBuildCacheServer import net.woggioni.gbcs.api.Configuration
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.MethodOrderer
@@ -19,7 +19,7 @@ abstract class AbstractServerTest {
protected lateinit var testDir : Path protected lateinit var testDir : Path
private var serverHandle : RemoteBuildCacheServer.ServerHandle? = null private var serverHandle : GradleBuildCacheServer.ServerHandle? = null
@BeforeAll @BeforeAll
fun setUp0(@TempDir tmpDir : Path) { fun setUp0(@TempDir tmpDir : Path) {
@@ -39,7 +39,7 @@ abstract class AbstractServerTest {
abstract fun tearDown() abstract fun tearDown()
private fun startServer(cfg : Configuration) { private fun startServer(cfg : Configuration) {
this.serverHandle = RemoteBuildCacheServer(cfg).run() this.serverHandle = GradleBuildCacheServer(cfg).run()
} }
private fun stopServer() { private fun stopServer() {

View File

@@ -1,13 +1,14 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.gbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.test.utils.CertificateUtils import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.test.utils.CertificateUtils.X509Credentials import net.woggioni.gbcs.server.test.utils.CertificateUtils
import net.woggioni.rbcs.server.test.utils.NetworkUtils import net.woggioni.gbcs.server.test.utils.CertificateUtils.X509Credentials
import net.woggioni.gbcs.server.test.utils.NetworkUtils
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
@@ -30,9 +31,9 @@ import kotlin.random.Random
abstract class AbstractTlsServerTest : AbstractServerTest() { abstract class AbstractTlsServerTest : AbstractServerTest() {
companion object { companion object {
private const val CA_CERTIFICATE_ENTRY = "rbcs-ca" private const val CA_CERTIFICATE_ENTRY = "gbcs-ca"
private const val CLIENT_CERTIFICATE_ENTRY = "rbcs-client" private const val CLIENT_CERTIFICATE_ENTRY = "gbcs-client"
private const val SERVER_CERTIFICATE_ENTRY = "rbcs-server" private const val SERVER_CERTIFICATE_ENTRY = "gbcs-server"
private const val PASSWORD = "password" private const val PASSWORD = "password"
} }
@@ -45,8 +46,8 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
private lateinit var trustStore: KeyStore private lateinit var trustStore: KeyStore
protected lateinit var ca: X509Credentials protected lateinit var ca: X509Credentials
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null, null) protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null)
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null, null) protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null)
protected val random = Random(101325) protected val random = Random(101325)
protected val keyValuePair = newEntry(random) protected val keyValuePair = newEntry(random)
private val serverPath : String? = null private val serverPath : String? = null

View File

@@ -1,9 +1,9 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

View File

@@ -1,10 +1,10 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import net.woggioni.rbcs.common.RBCS.toUrl import net.woggioni.gbcs.common.GBCS.toUrl
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory import net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.rbcs.server.configuration.Parser import net.woggioni.gbcs.server.configuration.Parser
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.gbcs.server.configuration.Serializer
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
@@ -17,18 +17,18 @@ class ConfigurationTest {
@ValueSource( @ValueSource(
strings = [ strings = [
"classpath:net/woggioni/rbcs/server/test/valid/rbcs-default.xml", "classpath:net/woggioni/gbcs/server/test/valid/gbcs-default.xml",
"classpath:net/woggioni/rbcs/server/test/valid/rbcs-memcached.xml", "classpath:net/woggioni/gbcs/server/test/valid/gbcs-memcached.xml",
"classpath:net/woggioni/rbcs/server/test/valid/rbcs-tls.xml", "classpath:net/woggioni/gbcs/server/test/valid/gbcs-tls.xml",
"classpath:net/woggioni/rbcs/server/test/valid/rbcs-memcached-tls.xml", "classpath:net/woggioni/gbcs/server/test/valid/gbcs-memcached-tls.xml",
] ]
) )
@ParameterizedTest @ParameterizedTest
fun test(configurationUrl: String, @TempDir testDir: Path) { fun test(configurationUrl: String, @TempDir testDir: Path) {
RbcsUrlStreamHandlerFactory.install() GbcsUrlStreamHandlerFactory.install()
val doc = Xml.parseXml(configurationUrl.toUrl()) val doc = Xml.parseXml(configurationUrl.toUrl())
val cfg = Parser.parse(doc) val cfg = Parser.parse(doc)
val configFile = testDir.resolve("rbcs.xml") val configFile = testDir.resolve("gbcs.xml")
Files.newOutputStream(configFile).use { Files.newOutputStream(configFile).use {
Xml.write(Serializer.serialize(cfg), it) Xml.write(Serializer.serialize(cfg), it)
} }
@@ -40,15 +40,15 @@ class ConfigurationTest {
@ValueSource( @ValueSource(
strings = [ strings = [
"classpath:net/woggioni/rbcs/server/test/invalid/invalid-user-ref.xml", "classpath:net/woggioni/gbcs/server/test/invalid/invalid-user-ref.xml",
"classpath:net/woggioni/rbcs/server/test/invalid/duplicate-anonymous-user.xml", "classpath:net/woggioni/gbcs/server/test/invalid/duplicate-anonymous-user.xml",
"classpath:net/woggioni/rbcs/server/test/invalid/duplicate-anonymous-user2.xml", "classpath:net/woggioni/gbcs/server/test/invalid/duplicate-anonymous-user2.xml",
"classpath:net/woggioni/rbcs/server/test/invalid/multiple-user-quota.xml", "classpath:net/woggioni/gbcs/server/test/invalid/multiple-user-quota.xml",
] ]
) )
@ParameterizedTest @ParameterizedTest
fun invalidConfigurationTest(configurationUrl: String) { fun invalidConfigurationTest(configurationUrl: String) {
RbcsUrlStreamHandlerFactory.install() GbcsUrlStreamHandlerFactory.install()
Assertions.assertThrows(SAXParseException::class.java) { Assertions.assertThrows(SAXParseException::class.java) {
Xml.parseXml(configurationUrl.toUrl()) Xml.parseXml(configurationUrl.toUrl())
} }

View File

@@ -1,8 +1,8 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

View File

@@ -1,7 +1,7 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

View File

@@ -1,11 +1,12 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.common.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.gbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.test.utils.NetworkUtils import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.gbcs.server.test.utils.NetworkUtils
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -51,8 +52,7 @@ class NoAuthServerTest : AbstractServerTest() {
maxAge = Duration.ofSeconds(3600 * 24), maxAge = Duration.ofSeconds(3600 * 24),
compressionEnabled = true, compressionEnabled = true,
digestAlgorithm = "MD5", digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION, compressionLevel = Deflater.DEFAULT_COMPRESSION
maxSize = 0x1000000
), ),
null, null,
null, null,

View File

@@ -1,12 +1,13 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.rbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.gbcs.api.Role
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.provider.ArgumentsSource
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse

View File

@@ -1,4 +1,4 @@
package net.woggioni.rbcs.server.test package net.woggioni.gbcs.server.test
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authorization> <authorization>
<users> <users>
<user name="user1" password="password1"/> <user name="user1" password="password1"/>
@@ -16,4 +16,4 @@
</anonymous> </anonymous>
</users> </users>
</authorization> </authorization>
</rbcs:server> </gbcs:server>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authorization> <authorization>
<users> <users>
<user name="user1" password="password1"/> <user name="user1" password="password1"/>
@@ -22,4 +22,4 @@
</group> </group>
</groups> </groups>
</authorization> </authorization>
</rbcs:server> </gbcs:server>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authorization> <authorization>
<users> <users>
<user name="user1" password="password1"/> <user name="user1" password="password1"/>
@@ -21,4 +21,4 @@
</group> </group>
</groups> </groups>
</authorization> </authorization>
</rbcs:server> </gbcs:server>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authorization> <authorization>
<users> <users>
<user name="user1" password="password1"> <user name="user1" password="password1">
@@ -12,4 +12,4 @@
</user> </user>
</users> </users>
</authorization> </authorization>
</rbcs:server> </gbcs:server>

Some files were not shown because too many files have changed in this diff Show More