Compare commits

...

6 Commits

Author SHA1 Message Date
17215b401a fixed shutdown issue
All checks were successful
CI / build (push) Successful in 16m6s
2025-03-03 22:02:00 +08:00
4aced1c717 moved to GraalVM CE
All checks were successful
CI / build (push) Successful in 15m34s
2025-03-03 17:53:12 +08:00
31ce34cddb simplified InMemoryCache implementation 2025-03-03 09:44:37 +08:00
d64f7f4f27 added performance benchmarks 2025-02-28 13:49:05 +08:00
d15235fc4c fixed missing plugin in native image
All checks were successful
CI / build (push) Successful in 34m59s
2025-02-27 23:30:33 +08:00
49bb4f41b8 improved documentation 2025-02-26 21:40:58 +08:00
13 changed files with 357 additions and 103 deletions

148
README.md
View File

@@ -1,4 +1,45 @@
# Remote Build Cache Server # Remote Build Cache Server
![Release](https://img.shields.io/gitea/v/release/woggioni/rbcs?gitea_url=https%3A%2F%2Fgitea.woggioni.net)
![Version](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fgitea.woggioni.net%2Fapi%2Fpackages%2Fwoggioni%2Fmaven%2Fnet%2Fwoggioni%2Frbcs-cli%2Fmaven-metadata.xml)
![License](https://img.shields.io/badge/license-MIT-green)
![Language](https://img.shields.io/gitea/languages/count/woggioni/rbcs?gitea_url=https%3A%2F%2Fgitea.woggioni.net)
<!--
![Last commit](https://img.shields.io/gitea/last-commit/woggioni/rbcs?gitea_url=https%3A%2F%2Fgitea.woggioni.net)
-->
Speed up your builds by sharing and reusing unchanged build outputs across your team.
Remote Build Cache Server (RBCS) allows teams to share and reuse unchanged build and test outputs,
significantly reducing build times for both local and CI environments. By eliminating redundant work,
RBCS helps teams become more productive and efficient.
**Key Features:**
- Support for both Gradle and Maven build environments
- Pluggable storage backends (in-memory, disk-backed, memcached)
- Flexible authentication (HTTP basic or TLS certificate)
- Role-based access control
- Request throttling
## Table of Contents
- [Quickstart](#quickstart)
- [Integration with build tools](#integration-with-build-tools)
- [Use RBCS with Gradle](#use-rbcs-with-gradle)
- [Use RBCS with Maven](#use-rbcs-with-maven)
- [Server configuration](#server-configuration)
- [Authentication](#authentication)
- [HTTP Basic authentication](#configure-http-basic-authentication)
- [TLS client certificate authentication](#configure-tls-certificate-authentication)
- [Authentication & Access Control](#access-control)
- [Plugins](#plugins)
- [Client Tools](#rbcs-client)
- [Logging](#logging)
- [Performance](#performance)
- [FAQ](#faq)
Remote Build Cache Server (shortened to RBCS) allows you to share and reuse unchanged build Remote Build Cache Server (shortened to RBCS) allows you to share and reuse unchanged build
and test outputs across the team. This speeds up local and CI builds since cycles are not wasted and test outputs across the team. This speeds up local and CI builds since cycles are not wasted
re-building components that are unaffected by new code changes. RBCS supports both Gradle and re-building components that are unaffected by new code changes. RBCS supports both Gradle and
@@ -12,7 +53,7 @@ and throttling.
## Quickstart ## Quickstart
### Downloading the jar file ### Use the all-in-one jar file
You can download the latest version from [this link](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/) You can download the latest version from [this link](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/)
@@ -25,7 +66,7 @@ java -jar rbcs-cli.jar server
By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication, By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication,
writing data to the disk, that you can use for testing writing data to the disk, that you can use for testing
### Using the Docker image ### Use the Docker image
You can pull the latest Docker image with You can pull the latest Docker image with
```bash ```bash
docker pull gitea.woggioni.net/woggioni/rbcs:latest docker pull gitea.woggioni.net/woggioni/rbcs:latest
@@ -34,41 +75,16 @@ docker pull gitea.woggioni.net/woggioni/rbcs:latest
By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication, By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication,
writing data to the disk, that you can use for testing writing data to the disk, that you can use for testing
### Using the native executable ### Use the native executable
If you are on a Linux X86_64 machine you can download the native executable If you are on a Linux X86_64 machine you can download the native executable
from [here](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/). from [here](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/).
It behaves the same as the jar file but it doesn't require a JVM and it has faster startup times. It behaves the same as the jar file but it doesn't require a JVM and it has faster startup times.
becausue of GraalVm's [closed-world assumption](https://www.graalvm.org/latest/reference-manual/native-image/basics/#static-analysis), because of GraalVM's [closed-world assumption](https://www.graalvm.org/latest/reference-manual/native-image/basics/#static-analysis),
the native executable does not supports plugins, so it comes with all plugins embedded into it. the native executable does not supports plugins, so it comes with all plugins embedded into it.
## Usage ## Integration with build tools
### Configuration ### Use RBCS with Gradle
The location of the `rbcs-server.xml` configuration file depends on the operating system,
Alternatively it can be changed setting the `RBCS_CONFIGURATION_DIR` environmental variable or `net.woggioni.rbcs.conf.dir` Java system property
to the directory that contain the `rbcs-server.xml` file.
The server configuration file follows the XML format and uses XML schema for validation
(you can find the schema for the main configuration file [here](https://gitea.woggioni.net/woggioni/rbcs/src/branch/master/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd)).
The configuration values are enclosed inside XML attribute and support system property / environmental variable interpolation.
As an example, you can configure RBCS to read the server port number from the `RBCS_SERVER_PORT` environmental variable
and the bind address from the `rbc.bind.address` JVM system property with
```xml
<bind host="${sys:rpc.bind.address}" port="${env:RBCS_SERVER_PORT}"/>
```
Full documentation for all tags and attributes is available [here](doc/server_configuration.md).
### Plugins
If you want to use memcache as a storage backend you'll also need to download [the memcache plugin](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-server-memcache/)
Plugins need to be stored in a folder named `plugins` in the located server's working directory
(the directory where the server process is started). They are shipped as TAR archives, so you need to extract
the content of the archive into the `plugins` directory for the server to pick them up.
### Using RBCS with Gradle
Add this to the `settings.gradle` file of your project Add this to the `settings.gradle` file of your project
@@ -113,7 +129,7 @@ add `org.gradle.caching=true` to your `<project>/gradle.properties` or run gradl
Read [Gradle documentation](https://docs.gradle.org/current/userguide/build_cache.html) for more detailed information. Read [Gradle documentation](https://docs.gradle.org/current/userguide/build_cache.html) for more detailed information.
### Using RBCS with Maven ### Use RBCS with Maven
1. Create an `extensions.xml` in `<project>/.mvn/extensions.xml` with the following content 1. Create an `extensions.xml` in `<project>/.mvn/extensions.xml` with the following content
```xml ```xml
@@ -143,6 +159,46 @@ Alternatively you can set those properties in your `<project>/pom.xml`
Read [here](https://maven.apache.org/extensions/maven-build-cache-extension/remote-cache.html) Read [here](https://maven.apache.org/extensions/maven-build-cache-extension/remote-cache.html)
for more informations for more informations
## Server configuration
RBCS reads an XML configuration file, by default named `rbcs-server.xml`.
The expected location of the `rbcs-server.xml` file depends on the operating system,
if the configuration file is not found a default one will be created and its location is printed
on the console
```bash
user@76a90cbcd75d:~$ rbcs-cli server
2025-01-01 00:00:00,000 [INFO ] (main) n.w.r.c.impl.commands.ServerCommand -- Creating default configuration file at '/home/user/.config/rbcs/rbcs-server.xml'
```
Alternatively it can be changed setting the `RBCS_CONFIGURATION_DIR` environmental variable or `net.woggioni.rbcs.conf.dir`
Java system property to the directory that contain the `rbcs-server.xml` file.
It can also be directly specified from the command line with
```bash
java -jar rbcs-cli.jar server -c /path/to/rbcs-server.xml
```
The server configuration file follows the XML format and uses XML schema for validation
(you can find the schema for the `rbcs-server.xml` configuration file [here](https://gitea.woggioni.net/woggioni/rbcs/src/branch/master/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd)).
The configuration values are enclosed inside XML attribute and support system property / environmental variable interpolation.
As an example, you can configure RBCS to read the server port number from the `RBCS_SERVER_PORT` environmental variable
and the bind address from the `rbc.bind.address` JVM system property with
```xml
<bind host="${sys:rpc.bind.address}" port="${env:RBCS_SERVER_PORT}"/>
```
Full documentation for all tags and attributes and configuration file examples
are available [here](doc/server_configuration.md).
### Plugins
If you want to use memcache as a storage backend you'll also need to download [the memcache plugin](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-server-memcache/)
Plugins need to be stored in a folder named `plugins` in the located server's working directory
(the directory where the server process is started). They are shipped as TAR archives, so you need to extract
the content of the archive into the `plugins` directory for the server to pick them up.
## Authentication ## Authentication
RBCS supports 2 authentication mechanisms: RBCS supports 2 authentication mechanisms:
@@ -250,7 +306,11 @@ as a health check (mind you need to have `Healthcheck` role in order to perform
RBCS ships with a command line client that can be used for testing, benchmarking or to manually RBCS ships with a command line client that can be used for testing, benchmarking or to manually
upload/download files to the cache. It must be configured with the `rbcs-client.xml`, upload/download files to the cache. It must be configured with the `rbcs-client.xml`,
whose location follows the same logic of the `rbcs-server.xml` whose location follows the same logic of the `rbcs-server.xml`.
The `rbcs-client.xml` must adhere to the [rbcs-client.xsd](rbcs-client/src/main/resources/net/woggioni/rbcs/client/schema/rbcs-client.xsd)
XML schema
The documentation for the `rbcs-client.xml` configuration file is available [here](conf/client_configuration.md)
### GET command ### GET command
@@ -263,6 +323,24 @@ java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME get -k $CACHE_KEY -v $FILE
```bash ```bash
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME put -k $CACHE_KEY -v $FILE_TO_BE_UPLOADED java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME put -k $CACHE_KEY -v $FILE_TO_BE_UPLOADED
``` ```
If you don't specify the key, a UUID key based on the file content will be used,
if you add the `-i` command line parameter, the uploaded file will be served with
`Content-Disposition: inline` HTTP header so that browser will attempt to render
it in the page instead of triggering a file download (in this way you can create a temporary web page).
The client will try to detect the file mime type upon upload but if you want to be sure you can specify
it manually with the `-t` parameter.
### Benchmark command
```bash
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME benchamrk -s 4096 -e 10000
```
This will insert 10000 randomly generates entries of 4096 bytes into RBCS, then retrieve them
and check that the retrieved value matches what was inserted.
It will also print throughput stats on the way.
## Logging ## Logging
RBCS uses [logback](https://logback.qos.ch/) and ships with a [default logging configuration](./conf/logback.xml) that RBCS uses [logback](https://logback.qos.ch/) and ships with a [default logging configuration](./conf/logback.xml) that
@@ -270,6 +348,10 @@ can be overridden with `-Dlogback.configurationFile=path/to/custom/configuration
[Logback documentation](https://logback.qos.ch/manual/configuration.html) for more details about [Logback documentation](https://logback.qos.ch/manual/configuration.html) for more details about
how to configure Logback how to configure Logback
## Performance
You can check performance benchmarks [here](doc/benchmarks.md)
## FAQ ## FAQ
### Why should I use a build cache? ### Why should I use a build cache?

View File

@@ -38,8 +38,8 @@ allprojects { subproject ->
withSourcesJar() withSourcesJar()
modularity.inferModulePath = true modularity.inferModulePath = true
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(23) languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ORACLE vendor = JvmVendorSpec.GRAAL_VM
} }
} }

87
doc/benchmarks.md Normal file
View File

@@ -0,0 +1,87 @@
# RBCS performance benchmarks
All test were executed under the following conditions:
- CPU: Intel Celeron J3455 (4 physical cores)
- memory: 8GB DDR3L 1600 MHz
- disk: SATA3 120GB SSD
- HTTP compression: disabled
- cache compression: disabled
- digest: none
- authentication: disabled
- TLS: disabled
- network RTT: 14ms
- network bandwidth: 112 MiB/s
### In memory cache backend
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|----------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 10 | 3691 | 4037 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 100 | 6881 | 7483 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 10 | 3790 | 4069 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 100 | 6716 | 7408 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 10 | 3399 | 1974 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 100 | 5341 | 6402 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 10 | 1099 | 1116 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 100 | 1379 | 1703 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 10 | 4443 | 5170 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 100 | 12813 | 13568 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 10 | 4450 | 4383 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 100 | 12212 | 13586 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 10 | 3441 | 3012 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 100 | 8982 | 10452 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 10 | 1391 | 1167 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 100 | 1303 | 1151 |
### Filesystem cache backend
compression: disabled
digest: none
authentication: disabled
TLS: disabled
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 10 | 1208 | 2048 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 1304 | 2394 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 1408 | 2157 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 1282 | 1888 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 1291 | 1256 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 1170 | 1423 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 313 | 606 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 298 | 609 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 2195 | 3477 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 2480 | 6207 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 2164 | 3413 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 2842 | 6218 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 1302 | 2591 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 2270 | 3045 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 375 | 394 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 364 | 462 |
### Memcache cache backend
compression: disabled
digest: MD5
authentication: disabled
TLS: disabled
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 10 | 2505 | 2578 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 3582 | 3935 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 2495 | 2784 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 3565 | 3883 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 2174 | 2505 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 2937 | 3563 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 648 | 1074 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 724 | 1548 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 2362 | 2927 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 5491 | 6531 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 2125 | 2807 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 5173 | 6242 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 1720 | 2397 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 3871 | 5859 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 616 | 1016 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 820 | 1677 |

View File

@@ -137,9 +137,22 @@ Configures TLS encryption.
read-timeout="PT5S" read-timeout="PT5S"
write-timeout="PT5S"/> write-timeout="PT5S"/>
<event-executor use-virtual-threads="true"/> <event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" /> <cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" />
<!--cache xs:type="rbcs:fileSystemCacheType" max-age="P7D" enable-compression="false" path="${sys:java.io.tmpdir}/rbcs"/-->
<authorization> <!-- uncomment this to enable the filesystem storage backend, sotring cache data in "${sys:java.io.tmpdir}/rbcs"
<cache xs:type="rbcs:fileSystemCacheType" max-age="P7D" enable-compression="false" path="${sys:java.io.tmpdir}/rbcs"/>
-->
<!-- uncomment this to use memcache as the storage backend, also make sure you have
the memcache plugin installed in the `plugins` directory if you are using running
the jar version of RBCS
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="0x1000" digest="MD5">
<server host="127.0.0.1" port="11211" max-connections="256"/>
</cache>
-->
<authorization>
<users> <users>
<user name="user1" password="II+qeNLft2pZ/JVNo9F7jpjM/BqEcfsJW27NZ6dPVs8tAwHbxrJppKYsbL7J/SMl"> <user name="user1" password="II+qeNLft2pZ/JVNo9F7jpjM/BqEcfsJW27NZ6dPVs8tAwHbxrJppKYsbL7J/SMl">
<quota calls="100" period="PT1S"/> <quota calls="100" period="PT1S"/>

View File

@@ -4,7 +4,7 @@ org.gradle.caching=true
rbcs.version = 0.2.0 rbcs.version = 0.2.0
lys.version = 2025.02.26 lys.version = 2025.03.03
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
docker.registry.url=gitea.woggioni.net docker.registry.url=gitea.woggioni.net

View File

@@ -12,6 +12,7 @@ plugins {
import net.woggioni.gradle.envelope.EnvelopePlugin import net.woggioni.gradle.envelope.EnvelopePlugin
import net.woggioni.gradle.envelope.EnvelopeJarTask import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
import net.woggioni.gradle.graalvm.NativeImageTask
import net.woggioni.gradle.graalvm.NativeImagePlugin import net.woggioni.gradle.graalvm.NativeImagePlugin
import net.woggioni.gradle.graalvm.UpxTask import net.woggioni.gradle.graalvm.UpxTask
import net.woggioni.gradle.graalvm.JlinkPlugin import net.woggioni.gradle.graalvm.JlinkPlugin
@@ -90,11 +91,6 @@ Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.E
} }
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) { tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ORACLE
}
mainClass = "net.woggioni.rbcs.cli.graal.GraalNativeImageConfiguration" mainClass = "net.woggioni.rbcs.cli.graal.GraalNativeImageConfiguration"
classpath = project.files( classpath = project.files(
configurations.configureNativeImageRuntimeClasspath, configurations.configureNativeImageRuntimeClasspath,
@@ -108,6 +104,10 @@ tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfi
} }
nativeImage { nativeImage {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = mainClassName mainClass = mainClassName
// mainModule = mainModuleName // mainModule = mainModuleName
useMusl = true useMusl = true

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs="urn:net.woggioni.rbcs.server"
xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache"
xs:schemaLocation="urn:net.woggioni.rbcs.server.memcache jpms://net.woggioni.rbcs.server.memcache/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd"
>
<bind host="127.0.0.1" port="8080" incoming-connections-backlog-size="1024"/>
<connection
max-request-size="67108864"
idle-timeout="PT10S"
read-idle-timeout="PT20S"
write-idle-timeout="PT20S"/>
<event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="0x1000" digest="MD5">
<server host="127.0.0.1" port="11211" max-connections="256"/>
</cache>
<!--cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" /-->
<!--cache xs:type="rbcs:fileSystemCacheType" max-age="P7D" enable-compression="false" /-->
<authorization>
<users>
<user name="woggioni" password="II+qeNLft2pZ/JVNo9F7jpjM/BqEcfsJW27NZ6dPVs8tAwHbxrJppKYsbL7J/SMl">
<quota calls="100" period="PT1S"/>
</user>
<user name="gitea" password="v6T9+q6/VNpvLknji3ixPiyz2YZCQMXj2FN7hvzbfc2Ig+IzAHO0iiBCH9oWuBDq"/>
<anonymous>
<quota calls="10" period="PT60S" initial-available-calls="10" max-available-calls="10"/>
</anonymous>
</users>
<groups>
<group name="readers">
<users>
<anonymous/>
</users>
<roles>
<reader/>
</roles>
</group>
<group name="writers">
<users>
<user ref="woggioni"/>
<user ref="gitea"/>
</users>
<roles>
<reader/>
<writer/>
</roles>
</group>
</groups>
</authorization>
<authentication>
<none/>
</authentication>
</rbcs:server>

View File

@@ -1,2 +1,2 @@
Args=-O3 --gc=serial --install-exit-handlers --initialize-at-run-time=io.netty --enable-url-protocols=jpms --initialize-at-build-time=net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory,net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory$JpmsHandler Args=-O3 -march=skylake --gc=serial --install-exit-handlers --initialize-at-run-time=io.netty --enable-url-protocols=jpms --initialize-at-build-time=net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory,net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory$JpmsHandler
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils #-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils

View File

@@ -36,6 +36,8 @@
"pattern":"\\Qnet/woggioni/rbcs/server/rbcs-default.xml\\E" "pattern":"\\Qnet/woggioni/rbcs/server/rbcs-default.xml\\E"
}, { }, {
"pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E" "pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E"
}, {
"pattern":"\\Q/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd\\E"
}, { }, {
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E" "pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
}]}, }]},

View File

@@ -32,8 +32,9 @@ object GraalNativeImageConfiguration {
@JvmStatic @JvmStatic
fun main(vararg args : String) { fun main(vararg args : String) {
val serverDoc = RemoteBuildCacheServer.DEFAULT_CONFIGURATION_URL.openStream().use { val serverURL = URI.create("file:conf/rbcs-client.xml").toURL()
Xml.parseXml(RemoteBuildCacheServer.DEFAULT_CONFIGURATION_URL, it) val serverDoc = serverURL.openStream().use {
Xml.parseXml(serverURL, it)
} }
Parser.parse(serverDoc) Parser.parse(serverDoc)

View File

@@ -9,6 +9,7 @@ import io.netty.channel.socket.SocketChannel
import net.woggioni.rbcs.api.CacheHandlerFactory import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.HostAndPort import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.server.memcache.client.MemcacheClient import net.woggioni.rbcs.server.memcache.client.MemcacheClient
import java.time.Duration import java.time.Duration
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -25,6 +26,10 @@ data class MemcacheCacheConfiguration(
val chunkSize: Int val chunkSize: Int
) : Configuration.Cache { ) : Configuration.Cache {
companion object {
private val log = createLogger<MemcacheCacheConfiguration>()
}
enum class CompressionMode { enum class CompressionMode {
/** /**
* Deflate mode * Deflate mode
@@ -69,15 +74,19 @@ data class MemcacheCacheConfiguration(
val pools = connectionPoolMap.values.toList() val pools = connectionPoolMap.values.toList()
val npools = pools.size val npools = pools.size
val finished = AtomicInteger(0) val finished = AtomicInteger(0)
pools.forEach { pool -> if (pools.isEmpty()) {
pool.closeAsync().addListener { complete(null)
if (!it.isSuccess) { } else {
failure.compareAndSet(null, it.cause()) pools.forEach { pool ->
} pool.closeAsync().addListener {
if(finished.incrementAndGet() == npools) { if (!it.isSuccess) {
when(val ex = failure.get()) { failure.compareAndSet(null, it.cause())
null -> complete(null) }
else -> completeExceptionally(ex) if (finished.incrementAndGet() == npools) {
when (val ex = failure.get()) {
null -> complete(null)
else -> completeExceptionally(ex)
}
} }
} }
} }

View File

@@ -368,13 +368,14 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
private val bossGroup: EventExecutorGroup, private val bossGroup: EventExecutorGroup,
private val executorGroups: Iterable<EventExecutorGroup>, private val executorGroups: Iterable<EventExecutorGroup>,
private val serverInitializer: AsyncCloseable, private val serverInitializer: AsyncCloseable,
) : Future<Void> by from(closeFuture, executorGroups, serverInitializer) { ) : Future<Void> by from(closeFuture, bossGroup, executorGroups, serverInitializer) {
companion object { companion object {
private val log = createLogger<ServerHandle>() private val log = createLogger<ServerHandle>()
private fun from( private fun from(
closeFuture: ChannelFuture, closeFuture: ChannelFuture,
bossGroup: EventExecutorGroup,
executorGroups: Iterable<EventExecutorGroup>, executorGroups: Iterable<EventExecutorGroup>,
serverInitializer: AsyncCloseable serverInitializer: AsyncCloseable
): CompletableFuture<Void> { ): CompletableFuture<Void> {
@@ -382,22 +383,15 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
closeFuture.addListener { closeFuture.addListener {
val errors = mutableListOf<Throwable>() val errors = mutableListOf<Throwable>()
val deadline = Instant.now().plusSeconds(20) val deadline = Instant.now().plusSeconds(20)
try {
serverInitializer.close()
} catch (ex: Throwable) {
log.error(ex.message, ex)
errors.addLast(ex)
}
serverInitializer.asyncClose().whenComplete { _, ex -> serverInitializer.asyncClose().whenCompleteAsync { _, ex ->
if(ex != null) { if(ex != null) {
log.error(ex.message, ex) log.error(ex.message, ex)
errors.addLast(ex) errors.addLast(ex)
} }
executorGroups.map { executorGroups.forEach(EventExecutorGroup::shutdownGracefully)
it.shutdownGracefully() bossGroup.terminationFuture().sync()
}
for (executorGroup in executorGroups) { for (executorGroup in executorGroups) {
val future = executorGroup.terminationFuture() val future = executorGroup.terminationFuture()

View File

@@ -6,11 +6,11 @@ import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.PriorityQueue
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock
private class CacheKey(private val value: ByteArray) { private class CacheKey(private val value: ByteArray) {
override fun equals(other: Any?) = if (other is CacheKey) { override fun equals(other: Any?) = if (other is CacheKey) {
@@ -34,15 +34,17 @@ class InMemoryCache(
private val log = createLogger<InMemoryCache>() private val log = createLogger<InMemoryCache>()
} }
private val size = AtomicLong() private var mapSize : Long = 0
private val map = ConcurrentHashMap<CacheKey, CacheEntry>() private val map = HashMap<CacheKey, CacheEntry>()
private val lock = ReentrantReadWriteLock()
private val cond = lock.writeLock().newCondition()
private class RemovalQueueElement(val key: CacheKey, val value: CacheEntry, val expiry: Instant) : private class RemovalQueueElement(val key: CacheKey, val value: CacheEntry, val expiry: Instant) :
Comparable<RemovalQueueElement> { Comparable<RemovalQueueElement> {
override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry) override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry)
} }
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>() private val removalQueue = PriorityQueue<RemovalQueueElement>()
@Volatile @Volatile
private var running = true private var running = true
@@ -51,20 +53,27 @@ class InMemoryCache(
init { init {
Thread.ofVirtual().name("in-memory-cache-gc").start { Thread.ofVirtual().name("in-memory-cache-gc").start {
try { try {
while (running) { lock.writeLock().withLock {
val el = removalQueue.poll(1, TimeUnit.SECONDS) ?: continue while (running) {
val value = el.value val el = removalQueue.poll()
val now = Instant.now() if(el == null) {
if (now > el.expiry) { cond.await(1000, TimeUnit.MILLISECONDS)
val removed = map.remove(el.key, value) continue
if (removed) { }
updateSizeAfterRemoval(value.content) val value = el.value
//Decrease the reference count for map val now = Instant.now()
value.content.release() if (now > el.expiry) {
val removed = map.remove(el.key, value)
if (removed) {
updateSizeAfterRemoval(value.content)
//Decrease the reference count for map
value.content.release()
}
} else {
removalQueue.offer(el)
val interval = minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1))
cond.await(interval.toMillis(), TimeUnit.MILLISECONDS)
} }
} else {
removalQueue.put(el)
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
} }
} }
complete(null) complete(null)
@@ -77,7 +86,7 @@ class InMemoryCache(
fun removeEldest(): Long { fun removeEldest(): Long {
while (true) { while (true) {
val el = removalQueue.take() val el = removalQueue.poll() ?: return mapSize
val value = el.value val value = el.value
val removed = map.remove(el.key, value) val removed = map.remove(el.key, value)
if (removed) { if (removed) {
@@ -90,18 +99,22 @@ class InMemoryCache(
} }
private fun updateSizeAfterRemoval(removed: ByteBuf): Long { private fun updateSizeAfterRemoval(removed: ByteBuf): Long {
return size.updateAndGet { currentSize: Long -> mapSize -= removed.readableBytes()
currentSize - removed.readableBytes() return mapSize
}
} }
override fun asyncClose() : CompletableFuture<Void> { override fun asyncClose() : CompletableFuture<Void> {
running = false running = false
lock.writeLock().withLock {
cond.signal()
}
return closeFuture return closeFuture
} }
fun get(key: ByteArray) = map[CacheKey(key)]?.run { fun get(key: ByteArray) = lock.readLock().withLock {
CacheEntry(metadata, content.retainedDuplicate()) map[CacheKey(key)]?.run {
CacheEntry(metadata, content.retainedDuplicate())
}
} }
fun put( fun put(
@@ -109,18 +122,18 @@ class InMemoryCache(
value: CacheEntry, value: CacheEntry,
) { ) {
val cacheKey = CacheKey(key) val cacheKey = CacheKey(key)
val oldSize = map.put(cacheKey, value)?.let { old -> lock.writeLock().withLock {
val result = old.content.readableBytes() val oldSize = map.put(cacheKey, value)?.let { old ->
old.content.release() val result = old.content.readableBytes()
result old.content.release()
} ?: 0 result
val delta = value.content.readableBytes() - oldSize } ?: 0
var newSize = size.updateAndGet { currentSize: Long -> val delta = value.content.readableBytes() - oldSize
currentSize + delta mapSize += delta
} removalQueue.offer(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
removalQueue.put(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge))) while (mapSize > maxSize) {
while (newSize > maxSize) { removeEldest()
newSize = removeEldest() }
} }
} }
} }