Compare commits

...

79 Commits

Author SHA1 Message Date
3774ab8ef0 updated Netty to 4.2.2
All checks were successful
CI / build (push) Successful in 5m52s
2025-06-10 16:34:16 +08:00
303828392e updated Netty to 4.2.1
All checks were successful
CI / build (push) Successful in 26m57s
2025-05-07 14:46:02 +08:00
5d8cbe34ef updated tomcat configuration 2025-05-01 03:42:37 +08:00
85c0d4a384 update Netty to 4.2.0 2025-04-16 01:06:28 +08:00
ae8817ad2a updated benchmarks 2025-03-24 15:01:56 +08:00
69f215e68f tuned GC parameters in Docker images
All checks were successful
CI / build (push) Successful in 16m35s
2025-03-24 14:42:04 +08:00
222b475223 ensured in-memory-cache is allocated to heap memory
All checks were successful
CI / build (push) Successful in 12m29s
2025-03-11 12:29:43 +08:00
ede515e2ca rebuild native image with wider ISA compatibility
All checks were successful
CI / build (push) Successful in 14m13s
2025-03-10 22:28:55 +08:00
974fdb7a91 added k8s benchmark files
All checks were successful
CI / build (push) Successful in 15m39s
2025-03-10 10:09:30 +08:00
a294229ff0 fixed memory leak in MemcachedCacheHandler 2025-03-09 22:16:03 +08:00
9600dd7e4f solved issue with ignored HttpContent and HttpCacheContent messages in the Netty pipeline
All checks were successful
CI / build (push) Successful in 12m48s
2025-03-09 13:57:52 +08:00
729276a2b1 fixed native image configuration
All checks were successful
CI / build (push) Successful in 14m12s
2025-03-08 14:35:50 +08:00
7ba7070693 fixed server support for request pipelining
All checks were successful
CI / build (push) Successful in 15m33s
2025-03-08 11:07:21 +08:00
59a12d6218 added server support for request pipelining
Some checks failed
CI / build (push) Has been cancelled
2025-03-07 11:53:42 +08:00
fc298de548 made chunk size a global shared parameter between the server and the cache backends 2025-03-06 22:08:19 +08:00
8b639fc0b3 added request pipelining support to RemoteBuildCacheClient 2025-03-06 21:58:53 +08:00
5545f618f9 updated documentation 2025-03-04 10:40:05 +08:00
43c0938d9a added test case 2025-03-04 09:35:26 +08:00
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
a1398045ac fixed native image configuration task
All checks were successful
CI / build (push) Successful in 37m16s
2025-02-26 17:42:02 +08:00
1f93602102 added healthcheck role
improved documentation

client configuration promoted to standalone class
2025-02-26 15:26:18 +08:00
c818463a2e fixed entrypoint in native Docker image 2025-02-25 22:26:29 +08:00
cd28563985 fixed typo 2025-02-25 22:06:52 +08:00
8ef2d9c64e improved usability of native docker image
All checks were successful
CI / build (push) Successful in 33m19s
2025-02-25 21:31:02 +08:00
1510956989 update JWO version to fix bug
All checks were successful
CI / build (push) Successful in 34m13s
the `RBCS_CONFIGURATION_DIR` was ignored because of a bug in JWO
2025-02-25 20:10:09 +08:00
ac4f0fdd19 increased tolerance of RetryTest 2025-02-25 19:19:07 +08:00
37da03c719 added signal handler to native executable
All checks were successful
CI / build (push) Successful in 33m50s
2025-02-25 19:15:48 +08:00
60bc4375cf update lys-catalog version 2025-02-25 15:54:11 +08:00
725fe22b80 added server configuration file documentation 2025-02-25 15:31:26 +08:00
ca18b63f27 added GraalVM native image executable build 2025-02-25 15:30:58 +08:00
23f2a351a6 shared event executor group between server and clients
All checks were successful
CI / build (push) Successful in 3m44s
- improved documentation
- closed memcache client's thread pools
2025-02-24 13:52:20 +08:00
c7d2b89d82 fixed server prefix handling 2025-02-22 15:25:41 +08:00
72c34b57a6 fixed memory leak 2025-02-22 15:25:41 +08:00
619873c4a9 removed readTimeout and writeTimeout from server configuration
added Markdown documentation
2025-02-22 15:25:36 +08:00
591f6e2af4 parametrized password hashing algorithm for basic authentication 2025-02-20 16:46:15 +08:00
ad00ebee9b made logback configuration file overridable in Docker image without changing ENTRYPOINT
All checks were successful
CI / build (push) Successful in 3m30s
2025-02-20 13:39:27 +08:00
adf8a0cf24 added documentation for rbcs-servlet
All checks were successful
CI / build (push) Successful in 1m54s
switched Docker images to serial GC
2025-02-19 23:00:56 +08:00
42eb26a948 optimize imports 2025-02-19 22:40:14 +08:00
f048a60540 implemented streaming request/response streaming
added metadata to cache values

added cache servlet for comparison
2025-02-19 22:37:54 +08:00
0463038aaa first commit with streaming support (buggy and unreliable) 2025-02-13 23:02:08 +08:00
7eca8a270d 0.1.6 release
All checks were successful
CI / build (push) Successful in 3m29s
2025-02-08 00:54:25 +08:00
84d7c977f9 added randomizer to retries 2025-02-07 23:19:13 +08:00
317eadce07 used virtual thread for garbage colection in FileSystemCache
All checks were successful
CI / build (push) Successful in 2m32s
2025-02-07 20:45:29 +08:00
af79e74b95 fixed max message size for memcache backend 2025-02-06 23:09:22 +08:00
78ae21caa4 0.1.4 release
All checks were successful
CI / build (push) Successful in 8m14s
2025-02-06 15:24:00 +08:00
6c0eadb9fb renamed project to "Remote Cache Build Server" (RBCS) 2025-02-06 15:20:50 +08:00
5fef1b932e updated lys-catalog version
All checks were successful
CI / build (push) Successful in 2m32s
2025-02-05 21:49:08 +08:00
5e173dbf62 fixed unit tests 2025-02-05 21:24:10 +08:00
53b24e3d54 improved benchmark accuracy 2025-02-05 19:10:25 +08:00
7d0f24fa58 fixed memory leak in InMemoryCache 2025-02-05 19:09:51 +08:00
1b6cf1bd96 fixed memory leak in memcached plugin 2025-02-05 14:41:11 +08:00
4180df2352 added healthcheck command to client 2025-02-05 00:02:17 +08:00
c2e388b931 switched to ZGC in docker image
All checks were successful
CI / build (push) Successful in 3m28s
2025-02-04 22:46:34 +08:00
6c62ac85c0 implemented memcached client with Netty
All checks were successful
CI / build (push) Successful in 1m46s
2025-02-04 22:09:28 +08:00
89153b60f8 fixed race condition in InMemoryCache 2025-02-01 10:14:13 +08:00
a2a40ab60f added semaphore to benchmark command 2025-01-28 00:00:07 +08:00
45458761f3 made TLS client certificate request from the server configurable
All checks were successful
CI / build (push) Successful in 4m2s
2025-01-27 13:32:04 +08:00
90a5834f5f added retry policy to gbcs-client 2025-01-27 13:12:12 +08:00
1823d0b9ca fixed throttling retry-after estimation
All checks were successful
CI / build (push) Successful in 3m9s
2025-01-25 01:25:01 +08:00
649cbba954 initial-available-calls is a positive integer
Some checks failed
CI / build (push) Failing after 2m49s
2025-01-24 21:19:40 +08:00
eb9ccce3be fixed exception handling in the client
Some checks failed
CI / build (push) Failing after 2m49s
2025-01-24 19:56:50 +08:00
316f64cf9d fixed bug with server timeouts
Some checks failed
CI / build (push) Failing after 2m47s
2025-01-24 18:48:25 +08:00
24a49779f9 fixed bug
Some checks failed
CI / build (push) Failing after 2m57s
2025-01-24 18:15:06 +08:00
423b749db9 added throttling
All checks were successful
CI / build (push) Successful in 2m39s
2025-01-24 16:53:19 +08:00
9ce3e7fa0a small refactor 2025-01-20 23:31:00 +08:00
1e6ece37a5 added remote address to logs
Some checks failed
CI / build (push) Failing after 1s
2025-01-20 20:50:48 +08:00
fc9900d821 fixed bug in the server configuration parser
All checks were successful
CI / build (push) Successful in 2m50s
added Jacoco test report
2025-01-20 20:23:09 +08:00
1a78c8092b fixed client bug (unhandled connection touts)
All checks were successful
CI / build (push) Successful in 3m7s
2025-01-20 19:18:20 +08:00
3d1847c408 added server timeouts
All checks were successful
CI / build (push) Successful in 3m18s
2025-01-20 15:45:13 +08:00
702556bfbb added parameter to configure incoming connections backlog size
All checks were successful
CI / build (push) Successful in 2m12s
2025-01-20 10:22:03 +08:00
06e9e7ca09 small optimization to make authenticator a singleton
All checks were successful
CI / build (push) Successful in 1m50s
2025-01-20 09:05:53 +08:00
fa5bb55baa uniformed xml configuration attributes, added max-request-size parameter 2025-01-20 08:24:44 +08:00
007d0fffd6 removed deployment related files 2025-01-17 22:42:11 +08:00
75ebf2248f general refactoring
All checks were successful
CI / build (push) Successful in 2m30s
2025-01-17 14:17:57 +08:00
199 changed files with 10363 additions and 2837 deletions

View File

@@ -0,0 +1,80 @@
name: CI
on:
push:
branches:
- 'dev'
jobs:
build:
runs-on: hostinger
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Execute Gradle build
run: ./gradlew build
- name: Prepare Docker image build
run: ./gradlew prepareDockerBuild
- name: Get project version
id: retrieve-version
run: ./gradlew -q version >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
- name: Login to Gitea container registry
uses: docker/login-action@v3
with:
registry: gitea.woggioni.net
username: woggioni
password: ${{ secrets.PUBLISHER_TOKEN }}
-
name: Build rbcs Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64,linux/arm64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:vanilla-dev
target: release-vanilla
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
-
name: Build rbcs memcache Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64,linux/arm64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:memcache-dev
target: release-memcache
cache-from: type=registry,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/rbcs:buildx
-
name: Build rbcs native Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:native-dev
target: release-native
-
name: Build rbcs jlink Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:jlink-dev
target: release-jlink

View File

@@ -9,11 +9,6 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: graalvm
java-version: 21
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v3
- name: Execute Gradle build - name: Execute Gradle build
@@ -36,7 +31,7 @@ jobs:
username: woggioni username: woggioni
password: ${{ secrets.PUBLISHER_TOKEN }} password: ${{ secrets.PUBLISHER_TOKEN }}
- -
name: Build gbcs Docker image name: Build rbcs 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"
@@ -44,12 +39,12 @@ jobs:
push: true push: true
pull: true pull: true
tags: | tags: |
gitea.woggioni.net/woggioni/gbcs:latest gitea.woggioni.net/woggioni/rbcs:vanilla
gitea.woggioni.net/woggioni/gbcs:${{ steps.retrieve-version.outputs.VERSION }} gitea.woggioni.net/woggioni/rbcs:vanilla-${{ steps.retrieve-version.outputs.VERSION }}
target: release target: release-vanilla
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
- -
name: Build gbcs memcached Docker image name: Build rbcs memcache 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"
@@ -57,11 +52,37 @@ jobs:
push: true push: true
pull: true pull: true
tags: | tags: |
gitea.woggioni.net/woggioni/gbcs:memcached gitea.woggioni.net/woggioni/rbcs:latest
gitea.woggioni.net/woggioni/gbcs:memcached-${{ steps.retrieve-version.outputs.VERSION }} gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
target: release-memcached gitea.woggioni.net/woggioni/rbcs:memcache
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/gbcs:buildx target: release-memcache
cache-from: type=registry,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/rbcs:buildx
-
name: Build rbcs native Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:native
gitea.woggioni.net/woggioni/rbcs:native-${{ steps.retrieve-version.outputs.VERSION }}
target: release-native
-
name: Build rbcs jlink Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:jlink
gitea.woggioni.net/woggioni/rbcs:jlink-${{ steps.retrieve-version.outputs.VERSION }}-jlink
target: release-jlink
- 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
gbcs-cli/native-image/*.json rbcs-cli/native-image/*.json

View File

@@ -1,2 +0,0 @@
FROM gitea.woggioni.net/woggioni/gbcs:memcached
COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 Y. T. CHUNG <zonyitoo@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

433
README.md Normal file
View File

@@ -0,0 +1,433 @@
# 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
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
Maven build tool environments.
It comes with pluggable storage backends, the core application offers in-memory storage or disk-backed storage,
in addition to this there is an official plugin to use memcached as the storage backend.
It supports HTTP basic authentication or, alternatively, TLS certificate authentication, role-based access control (RBAC),
and throttling.
## Quickstart
### 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/)
Assuming you have Java 21 or later installed, you can launch the server directly with
```bash
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,
writing data to the disk, that you can use for testing
### Use the Docker image
You can pull the latest Docker image with
```bash
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,
writing data to the disk, that you can use for testing
### Use 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/).
It behaves the same as the jar file but it doesn't require a JVM and it has faster startup times.
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.
> [!WARNING]
> The native executable is built with `-march=skylake`, so it may fail with SIGILL on x86 CPUs that do not support
> the full skylake instruction set (as a rule of thumb, older than 2015)
## Integration with build tools
### Use RBCS with Gradle
Add this to the `settings.gradle` file of your project
```groovy
buildCache {
remote(HttpBuildCache) {
url = 'https://rbcs.example.com/'
push = true
allowInsecureProtocol = false
// The credentials block is only required if you enable
// HTTP basic authentication on RBCS
credentials {
username = 'build-cache-user'
password = 'some-complicated-password'
}
}
}
```
alternatively you can add this to `${GRADLE_HOME}/init.gradle` to configure the remote cache
at the system level
```groovy
gradle.settingsEvaluated { settings ->
settings.buildCache {
remote(HttpBuildCache) {
url = 'https://rbcs.example.com/'
push = true
allowInsecureProtocol = false
// The credentials block is only required if you enable
// HTTP basic authentication on RBCS
credentials {
username = 'build-cache-user'
password = 'some-complicated-password'
}
}
}
}
```
add `org.gradle.caching=true` to your `<project>/gradle.properties` or run gradle with `--build-cache`.
Read [Gradle documentation](https://docs.gradle.org/current/userguide/build_cache.html) for more detailed information.
### Use RBCS with Maven
1. Create an `extensions.xml` in `<project>/.mvn/extensions.xml` with the following content
```xml
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.1.0 https://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
<extension>
<groupId>org.apache.maven.extensions</groupId>
<artifactId>maven-build-cache-extension</artifactId>
<version>1.2.0</version>
</extension>
</extensions>
```
2. Copy [maven-build-cache-config.xml](https://maven.apache.org/extensions/maven-build-cache-extension/maven-build-cache-config.xml) into `<project>/.mvn/` folder
3. Edit the `cache/configuration/remote` element
```xml
<remote enabled="true" id="rbcs">
<url>https://rbcs.example.com/</url>
</remote>
```
4. Run maven with
```bash
mvn -Dmaven.build.cache.enabled=true -Dmaven.build.cache.debugOutput=true -Dmaven.build.cache.remote.save.enabled=true package
```
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)
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
RBCS supports 2 authentication mechanisms:
- HTTP basic authentication
- TLS certificate authentication
### Configure HTTP basic authentication
Add a `<basic>` element to the `<authentication>` element in your `rbcs-server.xml`
```xml
<authentication>
<basic/>
</authentication>
```
### Configure TLS certificate authentication
Add a `<client-certificate>` element to the `<authentication>` element in your `rbcs-server.xml`
```xml
<authentication>
<client-certificate>
<user-extractor attribute-name="CN" pattern="(.*)"/>
<group-extractor attribute-name="O" pattern="(.*)"/>
</client-certificate>
</authentication>
```
The `<user-extractor>` here determines how the username is extracted from the
subject's X.500 name in the TLS certificate presented by the client, where `attribute-name`
is the `RelativeDistinguishedName` (RDN) identifier and pattern is a regular expression
that will be applied to extract the username from the first group present in the regex.
An error will be thrown if the regular expression contains no groups, while additional
groups are ignored.
Similarly, the `<group-extractor>` here determines how the group name is extracted from the
subject's X.500 name in the TLS certificate presented by the client.
Note that this allows to assign roles to incoming requests without necessarily assigning them
a username.
## Access control
RBCS supports role-based access control (RBAC), three roles are available:
- `Reader` can perform `GET` calls
- `Writer` can perform `PUT` calls
- `Healthcheck` can perform `TRACE` calls
Roles are assigned to groups so that a user will have a role only if that roles belongs
to one of the groups he is a member of.
There is also a special `<anonymous>` user
which matches any request who hasn't been authenticated and that can be assigned
to any group like a normal user. This permits to have a build cache that is
publicly readable but only writable by authenticated users (e.g. CI/CD pipeline).
### Defining users
Users can be defined in the `<authorization>` element
```xml
<authorization>
<users>
<user name="user1" password="kb/vNnkn2RvyPkTN6Q07uH0F7wI7u61MkManD3NHregRukBg4KHehfbqtLTb39fZjHA+SRH+EpEWDCf+Rihr5H5C1YN5qwmArV0p8O5ptC4="/>
<user name="user2" password="2J7MAhdIzZ3SO+JGB+K6wPhb4P5LH1L4L7yJCl5QrxNfAWRr5jTUExJRbcgbH1UfnkCbIO1p+xTDq+FCj3LFBZeMZUNZ47npN+WR7AX3VTo="/>
<anonymous/>
</users>
<groups>
<group name="readers">
<users>
<anonymous/>
</users>
<roles>
<reader/>
</roles>
</group>
<group name="writers">
<users>
<user ref="user1"/>
<user ref="user2"/>
</users>
<roles>
<reader/>
<writer/>
<healthcheck/>
</roles>
</group>
</groups>
</authorization>
```
The `password` attribute is only used for HTTP Basic authentication, so it can be omitted
if you use TLS certificate authentication. It must contain a password hash that can be derived from
the actual password using the following command
```bash
java -jar rbcs-cli.jar password
```
## Reliability
RBCS implements the [TRACE](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE) HTTP method and this functionality can be used
as a health check (mind you need to have `Healthcheck` role in order to perform it and match the server's `prefix` in the URL).
## RBCS Client
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`,
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
```bash
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME get -k $CACHE_KEY -v $FILE_WHERE_THE_VALUE_WILL_BE_STORED
```
### PUT command
```bash
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
RBCS uses [logback](https://logback.qos.ch/) and ships with a [default logging configuration](./conf/logback.xml) that
can be overridden with `-Dlogback.configurationFile=path/to/custom/configuration.xml`, refer to
[Logback documentation](https://logback.qos.ch/manual/configuration.html) for more details about
how to configure Logback
## Performance
You can check performance benchmarks [here](doc/benchmarks.md)
## FAQ
### Why should I use a build cache?
#### Build Caches Improve Build & Test Performance
Building software consists of a number of steps, like compiling sources, executing tests, and linking binaries. Weve seen that a binary artifact repository helps when such a step requires an external component by downloading the artifact from the repository rather than building it locally.
However, there are many additional steps in this build process which can be optimized to reduce the build time. An obvious strategy is to avoid executing build steps which dominate the total build time when these build steps are not needed.
Most build times are dominated by the testing step.
While binary repositories cannot capture the outcome of a test build step (only the test reports
when included in binary artifacts), build caches are designed to eliminate redundant executions
for every build step. Moreover, it generalizes the concept of avoiding work associated with any
incremental step of the build, including test execution, compilation and resource processing.
The mechanism itself is comparable to a pure function. That is, given some inputs such as source
files and environment parameters we know that the output is always going to be the same.
As a result, we can cache it and retrieve it based on a simple cryptographic hash of the inputs.
Build caching is supported natively by some build tools.
#### Improve CI builds with a remote build cache
When analyzing the role of a build cache it is important to take into account the granularity
of the changes that it caches. Imagine a full build for a project with 40 to 50 modules
which fails at the last step (deployment) because the staging environment is temporarily unavailable.
Although the vast majority of the build steps (potentially thousands) succeed,
the change can not be deployed to the staging environment.
Without a build cache one typically relies on a very complex CI configuration to reuse build step outputs
or would have to repeat the full build once the environment is available.
Some build tools dont support incremental builds properly. For example, outputs of a build started
from scratch may vary when compared to subsequent builds that rely on the initial builds output.
As a result, to preserve build integrity, its crucial to rebuild from scratch, or cleanly, in this
scenario.
With a build cache, only the last step needs to be executed and the build can be re-triggered
when the environment is back online. This automatically saves all of the time and
resources required across the different build steps which were successfully executed.
Instead of executing the intermediate steps, the build tool pulls the outputs from the build cache,
avoiding a lot of redundant work
#### Share outputs with a remote build cache
One of the most important advantages of a remote build cache is the ability to share build outputs.
In most CI configurations, for example, a number of pipelines are created.
These may include one for building the sources, one for testing, one for publishing the outcomes
to a remote repository, and other pipelines to test on different platforms.
There are even situations where CI builds partially build a project (i.e. some modules and not others).
Most of those pipelines share a lot of intermediate build steps. All builds which perform testing
require the binaries to be ready. All publishing builds require all previous steps to be executed.
And because modern CI infrastructure means executing everything in containerized (isolated) environments,
significant resources are wasted by repeatedly building the same intermediate artifacts.
A remote build cache greatly reduces this overhead by orders of magnitudes because it provides a way
for all those pipelines to share their outputs. After all, there is no point recreating an output that
is already available in the cache.
Because there are inherent dependencies between software components of a build,
introducing a build cache dramatically reduces the impact of exploding a component into multiple pieces,
allowing for increased modularity without increased overhead.
#### Make local developers more efficient with remote build caches
It is common for different teams within a company to work on different modules of a single large
application. In this case, most teams dont care about building the other parts of the software.
By introducing a remote cache developers immediately benefit from pre-built artifacts when checking out code.
Because it has already been built on CI, they dont have to do it locally.
Introducing a remote cache is a huge benefit for those developers. Consider that a typical developers
day begins by performing a code checkout. Most likely the checked out code has already been built on CI.
Therefore, no time is wasted running the first build of the day. The remote cache provides all of the
intermediate artifacts needed. And, in the event local changes are made, the remote cache still leverages
partial cache hits for projects which are independent. As other developers in the organization request
CI builds, the remote cache continues to populate, increasing the likelihood of these remote cache hits
across team members.

View File

@@ -0,0 +1,94 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: rbcs-server
data:
rbcs-server.xml: |
<?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="0.0.0.0" port="8080" incoming-connections-backlog-size="128"/>
<connection
max-request-size="0xd000000"
idle-timeout="PT15S"
read-idle-timeout="PT30S"
write-idle-timeout="PT30S"/>
<event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:fileSystemCacheType" max-age="P7D" enable-compression="false" path="/home/luser/cache" digest="SHA-224"/>
</rbcs:server>
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rbcs-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 16Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rbcs-deployment
labels:
app: rbcs
spec:
replicas: 1
selector:
matchLabels:
app: rbcs
template:
metadata:
labels:
app: rbcs
spec:
containers:
- name: rbcs
image: gitea.woggioni.net/woggioni/rbcs:memcache
imagePullPolicy: Always
command: ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=75","-jar", "/home/luser/rbcs.jar"]
args: ['server', '-c', 'rbcs-server.xml']
ports:
- containerPort: 8080
volumeMounts:
- name: config-volume
mountPath: /home/luser/rbcs-server.xml
subPath: rbcs-server.xml
- name: cache-volume
mountPath: /home/luser/cache
resources:
requests:
memory: "0.25Gi"
cpu: "1"
limits:
memory: "0.5Gi"
cpu: "1"
volumes:
- name: config-volume
configMap:
name: rbcs-server
- name: cache-volume
persistentVolumeClaim:
claimName: rbcs-pvc
---
apiVersion: v1
kind: Service
metadata:
name: rbcs-service
spec:
type: LoadBalancer
ports:
- port: 8080
targetPort: 8080
protocol: TCP
selector:
app: rbcs

View File

@@ -0,0 +1,77 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: rbcs-server
data:
rbcs-server.xml: |
<?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="0.0.0.0" port="8080" incoming-connections-backlog-size="128"/>
<connection
max-request-size="0xd000000"
idle-timeout="PT15S"
read-idle-timeout="PT30S"
write-idle-timeout="PT30S"/>
<event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x40000000" digest="SHA-224"/>
</rbcs:server>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rbcs-deployment
labels:
app: rbcs
spec:
replicas: 1
selector:
matchLabels:
app: rbcs
template:
metadata:
labels:
app: rbcs
spec:
containers:
- name: rbcs
image: gitea.woggioni.net/woggioni/rbcs:memcache
imagePullPolicy: Always
command: ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=75","-jar", "/home/luser/rbcs.jar"]
args: ['server', '-c', 'rbcs-server.xml']
ports:
- containerPort: 8080
volumeMounts:
- name: config-volume
mountPath: /home/luser/rbcs-server.xml
subPath: rbcs-server.xml
resources:
requests:
memory: "0.5Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "1"
volumes:
- name: config-volume
configMap:
name: rbcs-server
---
apiVersion: v1
kind: Service
metadata:
name: rbcs-service
spec:
type: LoadBalancer
ports:
- port: 8080
targetPort: 8080
protocol: TCP
selector:
app: rbcs

118
benchmark/rbcs-memcache.yml Normal file
View File

@@ -0,0 +1,118 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: rbcs-server
data:
rbcs-server.xml: |
<?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="0.0.0.0" port="8080" incoming-connections-backlog-size="128"/>
<connection
max-request-size="0xd000000"
idle-timeout="PT15S"
read-idle-timeout="PT30S"
write-idle-timeout="PT30S"/>
<event-executor use-virtual-threads="true"/>
<!--cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" /-->
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="0x1000" digest="SHA-224">
<server host="memcached-service" port="11211" max-connections="256"/>
</cache>
</rbcs:server>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rbcs-deployment
labels:
app: rbcs
spec:
replicas: 1
selector:
matchLabels:
app: rbcs
template:
metadata:
labels:
app: rbcs
spec:
containers:
- name: rbcs
image: gitea.woggioni.net/woggioni/rbcs:memcache
imagePullPolicy: Always
command: ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=75","-jar", "/home/luser/rbcs.jar"]
args: ['server', '-c', 'rbcs-server.xml']
ports:
- containerPort: 8080
volumeMounts:
- name: config-volume
mountPath: /home/luser/rbcs-server.xml
subPath: rbcs-server.xml
resources:
requests:
memory: "0.5Gi"
cpu: "1"
limits:
memory: "0.5Gi"
cpu: "3.5"
volumes:
- name: config-volume
configMap:
name: rbcs-server
---
apiVersion: v1
kind: Service
metadata:
name: rbcs-service
spec:
type: LoadBalancer
ports:
- port: 8080
targetPort: 8080
protocol: TCP
selector:
app: rbcs
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: memcached-deployment
spec:
replicas: 1
selector:
matchLabels:
app: memcached
template:
metadata:
labels:
app: memcached
spec:
containers:
- name: memcached
image: memcached
args: ["-I", "128m", "-m", "4096", "-t", "1"]
resources:
requests:
memory: "1Gi"
cpu: "500m" # 0.5 CPU
limits:
memory: "5Gi"
cpu: "500m" # 0.5 CP
---
apiVersion: v1
kind: Service
metadata:
name: memcached-service
spec:
type: ClusterIP # ClusterIP makes it accessible only within the cluster
ports:
- port: 11211 # Default memcached port
targetPort: 11211
protocol: TCP
selector:
app: memcached

View File

@@ -1,9 +1,7 @@
plugins { plugins {
id 'java-library' alias catalog.plugins.kotlin.jvm apply false
alias catalog.plugins.kotlin.jvm
alias catalog.plugins.sambal alias catalog.plugins.sambal
alias catalog.plugins.lombok alias catalog.plugins.lombok apply false
id 'maven-publish'
} }
@@ -16,9 +14,7 @@ allprojects { subproject ->
if(project.currentTag.isPresent()) { if(project.currentTag.isPresent()) {
version = project.currentTag.map { it[0] }.get() version = project.currentTag.map { it[0] }.get()
} else { } else {
version = project.gitRevision.map { gitRevision -> version = "${getProperty('rbcs.version')}-SNAPSHOT"
"${getProperty('gbcs.version')}.${gitRevision[0..10]}"
}.get()
} }
repositories { repositories {
@@ -26,7 +22,6 @@ allprojects { subproject ->
url = getProperty('gitea.maven.url') url = getProperty('gitea.maven.url')
content { content {
includeModule 'net.woggioni', 'jwo' includeModule 'net.woggioni', 'jwo'
includeModule 'net.woggioni', 'xmemcached'
includeGroup 'com.lys' includeGroup 'com.lys'
} }
} }
@@ -44,10 +39,15 @@ allprojects { subproject ->
modularity.inferModulePath = true modularity.inferModulePath = true
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(21) languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ORACLE
} }
} }
dependencies {
testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params
testRuntimeOnly catalog.junit.jupiter.engine
}
test { test {
useJUnitPlatform() useJUnitPlatform()
} }
@@ -68,6 +68,15 @@ allprojects { subproject ->
} }
} }
pluginManager.withPlugin('jacoco') {
test {
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
}
}
pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) { pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) {
tasks.withType(KotlinCompile.class) { tasks.withType(KotlinCompile.class) {
compilerOptions.jvmTarget = JvmTarget.JVM_21 compilerOptions.jvmTarget = JvmTarget.JVM_21
@@ -102,34 +111,6 @@ allprojects { subproject ->
} }
} }
dependencies {
implementation catalog.jwo
implementation catalog.slf4j.api
implementation catalog.netty.codec.http
api project('gbcs-base')
api project('gbcs-api')
// runtimeOnly catalog.slf4j.jdk14
testRuntimeOnly catalog.logback.classic
testImplementation catalog.bcprov.jdk18on
testImplementation catalog.bcpkix.jdk18on
testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params
testRuntimeOnly catalog.junit.jupiter.engine
testRuntimeOnly project("gbcs-memcached")
}
publishing {
publications {
maven(MavenPublication) {
from(components["java"])
}
}
}
tasks.register('version') { tasks.register('version') {
doLast { doLast {
println("VERSION=$version") println("VERSION=$version")

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="true" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs"
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached"
xs:schemaLocation="urn:net.woggioni.gbcs-memcached jpms://net.woggioni.gbcs.memcached/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd">
<bind host="0.0.0.0" port="13080" />
<cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="16777216" compression-mode="zip">
<server host="memcached" port="11211"/>
</cache>
<authentication>
<none/>
</authentication>
</gbcs:server>

View File

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

86
doc/benchmarks.md Normal file
View File

@@ -0,0 +1,86 @@
# 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 | 7867 | 13762 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 100 | 7728 | 14180 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 10 | 7964 | 10992 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 100 | 8415 | 12478 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 10 | 4268 | 5395 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 100 | 5585 | 8259 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 10 | 1063 | 1185 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 100 | 1522 | 1366 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 10 | 11271 | 14092 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 100 | 16064 | 24201 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 10 | 11504 | 13077 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 100 | 17379 | 22094 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 10 | 9151 | 9489 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 100 | 13194 | 18268 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 10 | 1590 | 1174 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 100 | 1539 | 1561 |
### 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.5 | 128 | 10 | 1478 | 5771 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 128 | 100 | 3166 | 8070 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 512 | 10 | 1717 | 5895 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 512 | 100 | 1125 | 6564 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 4096 | 10 | 819 | 2509 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 4096 | 100 | 1136 | 2365 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 65536 | 10 | 584 | 632 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 65536 | 100 | 529 | 635 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 128 | 10 | 1227 | 3342 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 128 | 100 | 1156 | 4035 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 512 | 10 | 979 | 3294 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 512 | 100 | 1217 | 3888 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 4096 | 10 | 535 | 1805 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 4096 | 100 | 555 | 1910 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 65536 | 10 | 301 | 494 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 65536 | 100 | 353 | 595 |
### 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 | 3380 | 6083 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 3323 | 4998 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 3924 | 6086 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 3440 | 5049 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 3347 | 5255 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 3685 | 4693 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 1304 | 1343 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 1481 | 1541 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 4667 | 7984 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 4044 | 8358 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 4177 | 7828 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 4079 | 8794 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 4588 | 6869 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 5343 | 7797 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 1624 | 1317 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 1633 | 1317 |

125
doc/client_configuration.md Normal file
View File

@@ -0,0 +1,125 @@
# XML Schema Documentation: RBCS Client Configuration
This document provides detailed information about the XML schema for RBCS client configuration, which defines profiles for connecting to RBCS servers.
## Root Element
### `profiles`
The root element that contains a collection of server profiles.
- **Type**: `profilesType`
- **Contains**: Zero or more `profile` elements
## Complex Types
### `profilesType`
Defines the structure for the profiles collection.
- **Elements**:
- `profile`: Server connection profile (0 to unbounded)
### `profileType`
Defines a server connection profile with authentication, connection settings, and retry policies.
- **Attributes**:
- `name` (required): Name of the server profile, referenced with the '-p' parameter in rbcs-cli
- `base-url` (required): RBCs server URL
- `max-connections`: Maximum number of concurrent TCP connections (default: 50)
- `connection-timeout`: Timeout for establishing connections
- `enable-compression`: Whether to enable HTTP compression (default: true)
- **Elements** (in sequence):
- **Authentication** (choice of one):
- `no-auth`: Disable authentication
- `basic-auth`: Enable HTTP basic authentication
- `tls-client-auth`: Enable TLS certificate authentication
- `connection` (optional): Connection timeout settings
- `retry-policy` (optional): Retry policy for failed requests
- `tls-trust-store` (optional): Custom truststore for server certificate validation
### `connectionType`
Defines connection timeout settings.
- **Attributes**:
- `idle-timeout`: Close connection after inactivity period (default: PT30S - 30 seconds)
- `read-idle-timeout`: Close connection when no read occurs (default: PT60S - 60 seconds)
- `write-idle-timeout`: Close connection when no write occurs (default: PT60S - 60 seconds)
### `noAuthType`
Indicates no authentication should be used.
- No attributes or elements
### `basicAuthType`
Configures HTTP Basic Authentication.
- **Attributes**:
- `user` (required): Username for authentication
- `password` (required): Password for authentication
### `tlsClientAuthType`
Configures TLS client certificate authentication.
- **Attributes**:
- `key-store-file` (required): Path to the keystore file
- `key-store-password` (required): Password to open the keystore
- `key-alias` (required): Alias of the keystore entry with the private key
- `key-password` (optional): Private key entry's encryption password
### `retryType`
Defines retry policy using exponential backoff.
- **Attributes**:
- `max-attempts` (required): Maximum number of retry attempts
- `initial-delay`: Delay before first retry (default: PT1S - 1 second)
- `exp`: Exponent for computing next delay (default: 2.0)
### `trustStoreType`
Configures custom truststore for server certificate validation.
- **Attributes**:
- `file` (required): Path to the truststore file
- `password`: Truststore file password
- `check-certificate-status`: Whether to check certificate validity using CRL/OCSP
- `verify-server-certificate`: Whether to validate server certificates (default: true)
## Sample XML Document
```xml
<?xml version="1.0" encoding="UTF-8"?>
<profiles xmlns="urn:net.woggioni.rbcs.client">
<!-- Profile with basic authentication -->
<profile name="production-server"
base-url="https://rbcs.example.com/api"
max-connections="100"
enable-compression="true">
<basic-auth user="admin" password="secure_password123"/>
<connection idle-timeout="PT45S"
read-idle-timeout="PT90S"
write-idle-timeout="PT90S"/>
<retry-policy max-attempts="5"
initial-delay="PT2S"
exp="1.5"/>
<tls-trust-store file="/path/to/truststore.jks"
password="truststore_password"
check-certificate-status="true"/>
</profile>
<!-- Profile with TLS client authentication -->
<profile name="secure-server"
base-url="https://secure.example.com/api"
max-connections="25">
<tls-client-auth key-store-file="/path/to/keystore.p12"
key-store-password="keystore_password"
key-alias="client-cert"
key-password="key_password"/>
<retry-policy max-attempts="3"/>
</profile>
<!-- Profile with no authentication -->
<profile name="development"
base-url="http://localhost:8080/api"
enable-compression="false">
<no-auth/>
</profile>
</profiles>
```
This sample XML document demonstrates three different profiles with various authentication methods and configuration options as defined in the schema.

189
doc/server_configuration.md Normal file
View File

@@ -0,0 +1,189 @@
### RBCS server configuration file elements and attributes
#### Root Element: `server`
The root element that contains all server configuration.
**Attributes:**
- `path` (optional): URI path prefix for cache requests. Example: if set to "cache", requests would be made to "http://www.example.com/cache/KEY"
#### Child Elements
#### `<bind>`
Configures server socket settings.
**Attributes:**
- `host` (required): Server bind address
- `port` (required): Server port number
- `incoming-connections-backlog-size` (optional, default: 1024): Maximum queue length for incoming connection indications
#### `<connection>`
Configures connection handling parameters.
**Attributes:**
- `idle-timeout` (optional, default: PT30S): Connection timeout when no activity
- `read-idle-timeout` (optional, default: PT60S): Connection timeout when no reads
- `write-idle-timeout` (optional, default: PT60S): Connection timeout when no writes
- `max-request-size` (optional, default: 0x4000000): Maximum allowed request body size
- `chunk-size` (default: 0x10000): Maximum socket write size
#### `<event-executor>`
Configures event execution settings.
**Attributes:**
- `use-virtual-threads` (optional, default: true): Whether to use virtual threads for the server handler
#### `<cache>`
Defines cache storage implementation. Two types are available:
##### InMemory Cache
A simple storage backend that uses an hash map to store data in memory
**Attributes:**
- `max-age` (default: P1D): Cache entry lifetime
- `max-size` (default: 0x1000000): Maximum cache size in bytes
- `digest` (default: MD5): Key hashing algorithm
- `enable-compression` (default: true): Enable deflate compression
- `compression-level` (default: -1): Compression level (-1 to 9)
##### FileSystem Cache
A storage backend that stores data in a folder on the disk
**Attributes:**
- `path`: Storage directory path
- `max-age` (default: P1D): Cache entry lifetime
- `digest` (default: MD5): Key hashing algorithm
- `enable-compression` (default: true): Enable deflate compression
- `compression-level` (default: -1): Compression level
#### `<authorization>`
Configures user and group-based access control.
##### `<users>`
List of registered users.
- Contains `<user>` elements:
**Attributes:**
- `name` (required): Username
- `password` (optional): For basic authentication
- Can contain an `anonymous` element to allow for unauthenticated access
##### `<groups>`
List of user groups.
- Contains `<group>` elements:
**Attributes:**
- `name`: Group name
- Can contain:
- `users`: List of user references
- `roles`: List of roles (READER/WRITER)
- `user-quota`: Per-user quota
- `group-quota`: Group-wide quota
#### `<authentication>`
Configures authentication mechanism. Options:
- `<basic>`: HTTP basic authentication
- `<client-certificate>`: TLS certificate authentication, it uses attributes of the subject's X.500 name
to extract the username and group of the client.
Example:
```xml
<client-certificate>
<user-extractor attribute-name="CN" pattern="(.*)"/>
<group-extractor attribute-name="O" pattern="(.*)"/>
</client-certificate>
```
- `<none>`: No authentication
#### `<tls>`
Configures TLS encryption.
**Child Elements:**
- `<keystore>`: Server certificate configuration
**Attributes:**
- `file` (required): Keystore file path
- `password`: Keystore password
- `key-alias` (required): Private key alias
- `key-password`: Private key password
- `<truststore>`: Client certificate verification
**Attributes:**
- `file` (required): Truststore file path
- `password`: Truststore password
- `check-certificate-status`: Enable CRL/OCSP checking
- `require-client-certificate` (default: false): Require client certificates
----------------------------
# Complete configuration example
```xml
<?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"
xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd"
>
<bind host="0.0.0.0" port="8080" incoming-connections-backlog-size="1024"/>
<connection
max-request-size="67108864"
idle-timeout="PT10S"
read-idle-timeout="PT20S"
write-idle-timeout="PT20S"
chunk-size="0x1000"/>
<event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" />
<!-- 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" digest="MD5">
<server host="127.0.0.1" port="11211" max-connections="256"/>
</cache>
-->
<authorization>
<users>
<user name="user1" password="II+qeNLft2pZ/JVNo9F7jpjM/BqEcfsJW27NZ6dPVs8tAwHbxrJppKYsbL7J/SMl">
<quota calls="100" period="PT1S"/>
</user>
<user name="user2" 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="user1"/>
<user ref="user2"/>
</users>
<roles>
<reader/>
<writer/>
</roles>
</group>
</groups>
</authorization>
<authentication>
<basic/>
</authentication>
</rbcs:server>
```

View File

@@ -1,36 +0,0 @@
networks:
default:
external: false
ipam:
driver: default
config:
- subnet: 172.118.0.0/16
ip_range: 172.118.0.0/16
gateway: 172.118.0.254
services:
gbcs:
build:
context: .
container_name: gbcs
restart: unless-stopped
ports:
- "127.0.0.1:8080:13080"
- "[::1]:8080:13080"
depends_on:
memcached:
condition: service_started
deploy:
resources:
limits:
cpus: "2.00"
memory: 256M
memcached:
image: memcached
container_name: memcached
restart: unless-stopped
command: -I 64m -m 900m
deploy:
resources:
limits:
cpus: "1.00"
memory: 1G

View File

@@ -1,21 +1,42 @@
FROM alpine:latest AS base-release FROM eclipse-temurin:21-jre-alpine AS base-release
RUN --mount=type=cache,target=/var/cache/apk apk update
RUN --mount=type=cache,target=/var/cache/apk apk add openjdk21-jre
RUN adduser -D luser RUN adduser -D luser
USER luser USER luser
WORKDIR /home/luser WORKDIR /home/luser
FROM base-release AS release FROM base-release AS release-vanilla
ADD gbcs-cli-envelope-*.jar gbcs.jar ADD rbcs-cli-envelope-*.jar rbcs.jar
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"] ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=70", "-XX:GCTimeRatio=24", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar"]
FROM base-release AS release-memcached FROM base-release AS release-memcache
ADD --chown=luser:luser gbcs-cli-envelope-*.jar gbcs.jar ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.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/gbcs-memcached*.tar RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar
WORKDIR /home/luser WORKDIR /home/luser
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"] ADD logback.xml .
ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=70", "-XX:GCTimeRatio=24", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar"]
FROM release-memcached as compose FROM busybox:musl AS base-native
COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml RUN mkdir -p /var/lib/rbcs /etc/rbcs
RUN adduser -D -u 1000 rbcs -h /var/lib/rbcs
FROM scratch AS release-native
COPY --from=base-native /etc/passwd /etc/passwd
COPY --from=base-native /etc/rbcs /etc/rbcs
COPY --from=base-native /var/lib/rbcs /var/lib/rbcs
ADD rbcs-cli.upx /usr/bin/rbcs-cli
ENV RBCS_CONFIGURATION_DIR="/etc/rbcs"
USER rbcs
WORKDIR /var/lib/rbcs
ENTRYPOINT ["/usr/bin/rbcs-cli", "-XX:MaximumHeapSizePercent=70"]
FROM debian:12-slim AS release-jlink
RUN mkdir -p /usr/share/java/rbcs
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-cli*.tar -C /usr/share/java/rbcs
ADD --chmod=755 rbcs-cli.sh /usr/local/bin/rbcs-cli
RUN adduser -u 1000 luser
USER luser
WORKDIR /home/luser
ADD logback.xml .
ENV JAVA_OPTS=-XX:-UseJVMCICompiler\ -Dlogback.configurationFile=logback.xml\ -XX:MaxRAMPercentage=70\ -XX:GCTimeRatio=24\ -XX:+UseZGC\ -XX:+ZGenerational
ENTRYPOINT ["/usr/local/bin/rbcs-cli"]

28
docker/README.md Normal file
View File

@@ -0,0 +1,28 @@
# RBCS Docker images
There are 3 image flavours:
- vanilla
- memcache
- native
The `vanilla` image only contains the envelope
jar file with no plugins and is based on `eclipse-temurin:21-jre-alpine`
The `memcache` image is similar to the `vanilla` image, except that it also contains
the `rbcs-server-memcache` plugin in the `plugins` folder, use this image if you don't want to use the `native`
image and want to use memcache as the cache backend
The `native` image contains a native, statically-linked executable created with GraalVM Native Image
that has no userspace dependencies. It also embeds the memcache plugin inside the executable.
Use this image for maximum efficiency and minimal memory footprint.
The `jlink` image contains a custom Java runtime created with GraalVM's Jlink
that only depends on glibc. It also contains the memcache plugin in the module path.
Use this image for best performance.
## Which image should I use?
The `native` image uses Java's SerialGC, so it's ideal for constrained environment like containers or small servers,
if you have a lot of resources and want to squeeze out the maximum throughput you should consider the
`vanilla` or `memcache` image, then choose and fine tune the garbage collector.
Also the `native` image is only available for the `x86_64` architecture at the moment,
while `vanilla` and `memcache` also ship a `aarch64` variant.

View File

@@ -18,8 +18,8 @@ configurations {
} }
dependencies { dependencies {
docker project(path: ':gbcs-cli', configuration: 'release') docker project(path: ':rbcs-cli', configuration: 'release')
docker project(path: ':gbcs-memcached', configuration: 'release') docker project(path: ':rbcs-server-memcache', configuration: 'release')
} }
Provider<Task> cleanTaskProvider = tasks.named(BasePlugin.CLEAN_TASK_NAME) {} Provider<Task> cleanTaskProvider = tasks.named(BasePlugin.CLEAN_TASK_NAME) {}
@@ -29,39 +29,40 @@ Provider<Copy> prepareDockerBuild = tasks.register('prepareDockerBuild', Copy) {
group = 'docker' group = 'docker'
into project.layout.buildDirectory.file('docker') into project.layout.buildDirectory.file('docker')
from(configurations.docker) from(configurations.docker)
from(file('Dockerfile')) from(files('Dockerfile', 'rbcs-cli.sh'))
from(rootProject.file('conf')) {
include 'logback.xml'
}
} }
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/gbcs:latest') images.add('gitea.woggioni.net/woggioni/rbcs:latest')
images.add("gitea.woggioni.net/woggioni/gbcs:${version}") images.add("gitea.woggioni.net/woggioni/rbcs:${version}")
} }
Provider<DockerTagImage> dockerTag = tasks.register('dockerTagImage', DockerTagImage) { Provider<DockerTagImage> dockerTag = tasks.register('dockerTagImage', DockerTagImage) {
group = 'docker' group = 'docker'
repository = 'gitea.woggioni.net/woggioni/gbcs' repository = 'gitea.woggioni.net/woggioni/rbcs'
imageId = 'gitea.woggioni.net/woggioni/gbcs:latest' imageId = 'gitea.woggioni.net/woggioni/rbcs:latest'
tag = version tag = version
} }
Provider<DockerTagImage> dockerTagMemcached = tasks.register('dockerTagMemcachedImage', DockerTagImage) { Provider<DockerTagImage> dockerTagMemcache = tasks.register('dockerTagMemcacheImage', DockerTagImage) {
group = 'docker' group = 'docker'
repository = 'gitea.woggioni.net/woggioni/gbcs' repository = 'gitea.woggioni.net/woggioni/rbcs'
imageId = 'gitea.woggioni.net/woggioni/gbcs:memcached' imageId = 'gitea.woggioni.net/woggioni/rbcs:memcache'
tag = "${version}-memcached" tag = "${version}-memcache"
} }
Provider<DockerPushImage> dockerPush = tasks.register('dockerPushImage', DockerPushImage) { Provider<DockerPushImage> dockerPush = tasks.register('dockerPushImage', DockerPushImage) {
group = 'docker' group = 'docker'
dependsOn dockerTag, dockerTagMemcached dependsOn dockerTag, dockerTagMemcache
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 }, dockerTagMemcached.flatMap{ it.tag }] images = [dockerTag.flatMap{ it.tag }, dockerTagMemcache.flatMap{ it.tag }]
} }

3
docker/rbcs-cli.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
DIR=/usr/share/java/rbcs
$DIR/bin/java $JAVA_OPTS -m net.woggioni.rbcs.cli "$@"

View File

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

View File

@@ -1,11 +0,0 @@
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,5 +0,0 @@
package net.woggioni.gbcs.api;
public enum Role {
Reader, Writer
}

View File

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

View File

@@ -1,9 +0,0 @@
module net.woggioni.gbcs.base {
requires java.xml;
requires java.logging;
requires org.slf4j;
requires kotlin.stdlib;
requires net.woggioni.jwo;
exports net.woggioni.gbcs.base;
}

View File

@@ -1,12 +0,0 @@
package net.woggioni.gbcs.base
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"
const val GBCS_PREFIX: String = "gbcs"
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
}

View File

@@ -1,111 +0,0 @@
package net.woggioni.gbcs.base
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isTraceEnabled) {
val (format, params) = messageBuilder()
trace(format, params)
}
}
inline fun Logger.debugParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isDebugEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.infoParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isInfoEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.warnParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isWarnEnabled) {
val (format, params) = messageBuilder()
warn(format, params)
}
}
inline fun Logger.errorParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isErrorEnabled) {
val (format, params) = messageBuilder()
error(format, params)
}
}
inline fun log(log : Logger,
filter : Logger.() -> Boolean,
loggerMethod : Logger.(String) -> Unit, messageBuilder : () -> String) {
if(log.filter()) {
log.loggerMethod(messageBuilder())
}
}
inline fun Logger.log(level : Level, messageBuilder : () -> String) {
if(isEnabledForLevel(level)) {
makeLoggingEventBuilder(level).log(messageBuilder())
}
}
inline fun Logger.trace(messageBuilder : () -> String) {
if(isTraceEnabled) {
trace(messageBuilder())
}
}
inline fun Logger.debug(messageBuilder : () -> String) {
if(isDebugEnabled) {
debug(messageBuilder())
}
}
inline fun Logger.info(messageBuilder : () -> String) {
if(isInfoEnabled) {
info(messageBuilder())
}
}
inline fun Logger.warn(messageBuilder : () -> String) {
if(isWarnEnabled) {
warn(messageBuilder())
}
}
inline fun Logger.error(messageBuilder : () -> String) {
if(isErrorEnabled) {
error(messageBuilder())
}
}
class LoggingConfig {
init {
val logManager = LogManager.getLogManager()
System.getProperty("log.config.source")?.let withSource@ { source ->
val urls = LoggingConfig::class.java.classLoader.getResources(source)
while(urls.hasMoreElements()) {
val url = urls.nextElement()
url.openStream().use { inputStream ->
logManager.readConfiguration(inputStream)
return@withSource
}
}
Path.of(source).takeIf(Files::exists)
?.let(Files::newInputStream)
?.use(logManager::readConfiguration)
}
}
}

View File

@@ -1,46 +0,0 @@
package net.woggioni.gbcs.base
import java.security.SecureRandom
import java.security.spec.KeySpec
import java.util.Base64
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
object PasswordSecurity {
private const val KEY_LENGTH = 256
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
val result = ByteArray(arr1.size + arr2.size)
var j = 0
for(element in arr1) {
result[j] = element
j += 1
}
for(element in arr2) {
result[j] = element
j += 1
}
return result
}
fun hashPassword(password : String, salt : String? = null) : String {
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
val result = ByteArray(16)
nextBytes(result)
result
}
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, 10, KEY_LENGTH)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val hash = factory.generateSecret(spec).encoded
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
}
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> {
val decoded = Base64.getDecoder().decode(passwordHash)
val hash = ByteArray(KEY_LENGTH / 8)
val salt = ByteArray(decoded.size - KEY_LENGTH / 8)
System.arraycopy(decoded, 0, hash, 0, hash.size)
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
return hash to salt
}
}

View File

@@ -1,89 +0,0 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
alias catalog.plugins.envelope
alias catalog.plugins.sambal
alias catalog.plugins.graalvm.native.image
alias catalog.plugins.graalvm.jlink
alias catalog.plugins.jpms.check
id 'maven-publish'
}
import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
import net.woggioni.gradle.graalvm.NativeImagePlugin
import net.woggioni.gradle.graalvm.NativeImageTask
import net.woggioni.gradle.graalvm.JlinkPlugin
import net.woggioni.gradle.graalvm.JlinkTask
Property<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.javaModuleMainClass = mainClassName
}
configurations {
release {
transitive = false
canBeConsumed = true
canBeResolved = true
visible = true
}
}
envelopeJar {
mainModule = 'net.woggioni.gbcs.cli'
mainClass = mainClassName
extraClasspath = ["plugins"]
}
dependencies {
implementation catalog.jwo
implementation catalog.slf4j.api
implementation catalog.netty.codec.http
implementation catalog.picocli
implementation project(":gbcs-client")
implementation rootProject
// runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic
}
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
// systemProperties['log.config.source'] = 'logging.properties'
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/cli/logback.xml'
}
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
mainClass = mainClassName
}
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
mainClass = mainClassName
useMusl = true
buildStaticImage = true
}
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
mainClass = mainClassName
mainModule = 'net.woggioni.gbcs.cli'
}
artifacts {
release(envelopeJarTaskProvider)
}
publishing {
publications {
maven(MavenPublication) {
artifact envelopeJar
}
}
}

View File

@@ -1,2 +0,0 @@
Args=-H:Optimize=3 --gc=serial
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils

View File

@@ -1,17 +0,0 @@
module net.woggioni.gbcs.cli {
requires org.slf4j;
requires net.woggioni.gbcs;
requires info.picocli;
requires net.woggioni.gbcs.base;
requires net.woggioni.gbcs.client;
requires kotlin.stdlib;
requires net.woggioni.jwo;
requires net.woggioni.gbcs.api;
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.base;
exports net.woggioni.gbcs.cli;
}

View File

@@ -1,59 +0,0 @@
package net.woggioni.gbcs.cli
import net.woggioni.gbcs.base.GbcsUrlStreamHandlerFactory
import net.woggioni.gbcs.base.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.PasswordHashCommand
import net.woggioni.gbcs.cli.impl.commands.ServerCommand
import net.woggioni.jwo.Application
import picocli.CommandLine
import picocli.CommandLine.Model.CommandSpec
@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())
})
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,132 +0,0 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.error
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GbcsClient
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.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 = GbcsClient(profile)
val entryGenerator = sequence {
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
while (true) {
val key = Base64.getUrlEncoder().encode(random.nextBytes(16)).toString(Charsets.UTF_8)
val value = random.nextBytes(0x1000)
yield(key to value)
}
}
val entries = let {
val completionQueue = LinkedBlockingQueue<Future<Pair<String, ByteArray>>>(numberOfEntries)
val start = Instant.now()
val totalElapsedTime = AtomicLong(0)
entryGenerator.take(numberOfEntries).forEach { entry ->
val requestStart = System.nanoTime()
val future = client.put(entry.first, entry.second).thenApply { entry }
future.whenComplete { _, _ ->
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
completionQueue.put(future)
}
}
val inserted = sequence<Pair<String, ByteArray>> {
var completionCounter = 0
while (completionCounter < numberOfEntries) {
val future = completionQueue.take()
try {
yield(future.get())
} catch (ee: ExecutionException) {
val cause = ee.cause ?: ee
log.error(cause.message, cause)
}
completionCounter += 1
}
}.toList()
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
"Insertion rate: ${numberOfEntries.toDouble() / elapsed * 1000} ops/s"
}
log.info {
"Average time per insertion: ${totalElapsedTime.get() / numberOfEntries.toDouble() * 1000} ms"
}
inserted
}
log.info {
"Inserted ${entries.size} entries"
}
if (entries.isNotEmpty()) {
val completionQueue = LinkedBlockingQueue<Future<Unit>>(entries.size)
val start = Instant.now()
val totalElapsedTime = AtomicLong(0)
entries.forEach { entry ->
val requestStart = System.nanoTime()
val future = client.get(entry.first).thenApply {
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
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 { _, _ ->
completionQueue.put(future)
}
}
var completionCounter = 0
while (completionCounter < entries.size) {
completionQueue.take()
completionCounter += 1
}
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
"Retrieval rate: ${entries.size.toDouble() / elapsed * 1000} ops/s"
}
log.info {
"Average time per retrieval: ${totalElapsedTime.get() / numberOfEntries.toDouble() * 1e6} ms"
}
} else {
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
}
}
}

View File

@@ -1,67 +0,0 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.GradleBuildCacheServer
import net.woggioni.gbcs.GradleBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import picocli.CommandLine
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
@CommandLine.Command(
name = "server",
description = ["GBCS server"],
showDefaultValues = true
)
class ServerCommand(app : Application) : GbcsCommand() {
private val log = contextLogger()
private fun createDefaultConfigurationFile(configurationFile: Path) {
log.info {
"Creating default configuration file at '$configurationFile'"
}
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
Files.newOutputStream(configurationFile).use { outputStream ->
defaultConfigurationFileResource.openStream().use { inputStream ->
JWO.copy(inputStream, outputStream)
}
}
}
@CommandLine.Option(
names = ["-c", "--config-file"],
description = ["Read the application configuration from this file"],
paramLabel = "CONFIG_FILE"
)
private var configurationFile: Path = findConfigurationFile(app, "gbcs-server.xml")
val configuration : Configuration by lazy {
GradleBuildCacheServer.loadConfiguration(configurationFile)
}
override fun run() {
if (!Files.exists(configurationFile)) {
Files.createDirectories(configurationFile.parent)
createDefaultConfigurationFile(configurationFile)
}
val configuration = GradleBuildCacheServer.loadConfiguration(configurationFile)
log.debug {
ByteArrayOutputStream().also {
GradleBuildCacheServer.dumpConfiguration(configuration, it)
}.let {
"Server configuration:\n${String(it.toByteArray())}"
}
}
val server = GradleBuildCacheServer(configuration)
server.run().use {
}
}
}

View File

@@ -1,15 +0,0 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
}
dependencies {
implementation project(':gbcs-api')
implementation project(':gbcs-base')
implementation catalog.picocli
implementation catalog.slf4j.api
implementation catalog.netty.buffer
implementation catalog.netty.codec.http
}

View File

@@ -1,254 +0,0 @@
package net.woggioni.gbcs.client
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPipeline
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.pool.AbstractChannelPoolHandler
import io.netty.channel.pool.ChannelPool
import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.DecoderException
import io.netty.handler.codec.http.DefaultFullHttpRequest
import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.HttpClientCodec
import io.netty.handler.codec.http.HttpContentDecompressor
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpObjectAggregator
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.Future
import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.client.impl.Parser
import java.net.InetSocketAddress
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
import io.netty.util.concurrent.Future as NettyFuture
class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable {
private val group: NioEventLoopGroup
private var sslContext: SslContext
private val log = contextLogger()
private val pool: ChannelPool
data class Configuration(
val profiles: Map<String, Profile>
) {
sealed class Authentication {
data class TlsClientAuthenticationCredentials(
val key: PrivateKey,
val certificateChain: Array<X509Certificate>
) : Authentication()
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
}
data class Profile(
val serverURI: URI,
val authentication: Authentication?,
val maxConnections : Int
)
companion object {
fun parse(path: Path): Configuration {
return Files.newInputStream(path).use {
Xml.parseXml(path.toUri().toURL(), it)
}.let(Parser::parse)
}
}
}
init {
group = NioEventLoopGroup()
sslContext = SslContextBuilder.forClient().also { builder ->
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
builder.keyManager(
tlsClientAuthenticationCredentials.key,
*tlsClientAuthenticationCredentials.certificateChain
)
}
}.build()
val (scheme, host, port) = profile.serverURI.run {
Triple(
if (scheme == null) "http" else profile.serverURI.scheme,
host,
port.takeIf { it > 0 } ?: if ("https" == scheme.lowercase()) 443 else 80
)
}
val bootstrap = Bootstrap().apply {
group(group)
channel(NioSocketChannel::class.java)
option(ChannelOption.TCP_NODELAY, true)
option(ChannelOption.SO_KEEPALIVE, true)
remoteAddress(InetSocketAddress(host, port))
}
val channelPoolHandler = object : AbstractChannelPoolHandler() {
@Volatile
private var connectionCount = AtomicInteger()
@Volatile
private var leaseCount = AtomicInteger()
override fun channelReleased(ch: Channel) {
log.debug {
"Released lease ${leaseCount.decrementAndGet()}"
}
}
override fun channelAcquired(ch: Channel?) {
log.debug {
"Acquired lease ${leaseCount.getAndIncrement()}"
}
}
override fun channelCreated(ch: Channel) {
log.debug {
"Created connection ${connectionCount.getAndIncrement()}"
}
val pipeline: ChannelPipeline = ch.pipeline()
// Add SSL handler if needed
if ("https".equals(scheme, ignoreCase = true)) {
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc(), host, port))
}
// HTTP handlers
pipeline.addLast("codec", HttpClientCodec())
pipeline.addLast("decompressor", HttpContentDecompressor())
pipeline.addLast("aggregator", HttpObjectAggregator(1048576))
pipeline.addLast("chunked", ChunkedWriteHandler())
}
}
pool = FixedChannelPool(bootstrap, channelPoolHandler, profile.maxConnections)
}
fun get(key: String): CompletableFuture<ByteArray?> {
return sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)
.thenApply {
val status = it.status()
if (it.status() == HttpResponseStatus.NOT_FOUND) {
null
} else 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 put(key: String, content: ByteArray): CompletableFuture<Unit> {
return sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content).thenApply {
val status = it.status()
if (it.status() != HttpResponseStatus.CREATED) {
throw HttpException(status)
}
}
}
private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?): CompletableFuture<FullHttpResponse> {
val responseFuture = CompletableFuture<FullHttpResponse>()
// Custom handler for processing responses
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
override fun operationComplete(channelFuture: Future<Channel>) {
if (channelFuture.isSuccess) {
val channel = channelFuture.now
val pipeline = channel.pipeline()
channel.pipeline().addLast("handler", object : SimpleChannelInboundHandler<FullHttpResponse>() {
override fun channelRead0(
ctx: ChannelHandlerContext,
response: FullHttpResponse
) {
responseFuture.complete(response)
pipeline.removeLast()
pool.release(channel)
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
val ex = when (cause) {
is DecoderException -> cause.cause
else -> cause
}
responseFuture.completeExceptionally(ex)
ctx.close()
pipeline.removeLast()
pool.release(channel)
}
})
// Prepare the HTTP request
val request: FullHttpRequest = let {
val content: ByteBuf? = body?.takeIf(ByteArray::isNotEmpty)?.let(Unpooled::wrappedBuffer)
DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
method,
uri.rawPath,
content ?: Unpooled.buffer(0)
).apply {
headers().apply {
if (content != null) {
set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM)
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
}
set(HttpHeaderNames.HOST, profile.serverURI.host)
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
set(
HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
)
// Add basic auth if configured
(profile.authentication as? Configuration.Authentication.BasicAuthenticationCredentials)?.let { credentials ->
val auth = "${credentials.username}:${credentials.password}"
val encodedAuth = Base64.getEncoder().encodeToString(auth.toByteArray())
set(HttpHeaderNames.AUTHORIZATION, "Basic $encodedAuth")
}
}
}
}
// Set headers
// Send the request
channel.writeAndFlush(request)
}
}
})
return responseFuture
}
fun shutDown(): NettyFuture<*> {
return group.shutdownGracefully()
}
override fun close() {
shutDown().sync()
}
}

View File

@@ -1,69 +0,0 @@
package net.woggioni.gbcs.client.impl
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.base.Xml.Companion.asIterable
import net.woggioni.gbcs.base.Xml.Companion.renderAttribute
import net.woggioni.gbcs.client.GbcsClient
import org.w3c.dom.Document
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.X509Certificate
object Parser {
fun parse(document: Document): GbcsClient.Configuration {
val root = document.documentElement
val profiles = mutableMapOf<String, GbcsClient.Configuration.Profile>()
for (child in root.asIterable()) {
val tagName = child.localName
when (tagName) {
"profile" -> {
val name = child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
val uri = child.renderAttribute("base-url")?.let(::URI) ?: throw ConfigurationException("base-url attribute is required")
var authentication: GbcsClient.Configuration.Authentication? = null
for (gchild in child.asIterable()) {
when (gchild.localName) {
"tls-client-auth" -> {
val keyStoreFile = gchild.renderAttribute("key-store-file")
val keyStorePassword =
gchild.renderAttribute("key-store-password")
val keyAlias = gchild.renderAttribute("key-alias")
val keyPassword = gchild.renderAttribute("key-password")
val keystore = KeyStore.getInstance("PKCS12").apply {
Files.newInputStream(Path.of(keyStoreFile)).use {
load(it, keyStorePassword?.toCharArray())
}
}
val key = keystore.getKey(keyAlias, keyPassword?.toCharArray()) as PrivateKey
val certChain = keystore.getCertificateChain(keyAlias).asSequence()
.map { it as X509Certificate }
.toList()
.toTypedArray()
authentication =
GbcsClient.Configuration.Authentication.TlsClientAuthenticationCredentials(key, certChain)
}
"basic-auth" -> {
val username = gchild.renderAttribute("user") ?: throw ConfigurationException("username attribute is required")
val password = gchild.renderAttribute("password") ?: throw ConfigurationException("password attribute is required")
authentication =
GbcsClient.Configuration.Authentication.BasicAuthenticationCredentials(username, password)
}
}
}
val maxConnections = child.renderAttribute("max-connections")
?.let(String::toInt)
?: 50
profiles[name] = GbcsClient.Configuration.Profile(uri, authentication, maxConnections)
}
}
}
return GbcsClient.Configuration(profiles)
}
}

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.gbcs.client"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
elementFormDefault="unqualified"
>
<xs:element name="profiles" type="gbcs-client:profilesType"/>
<xs:complexType name="profilesType">
<xs:sequence minOccurs="0">
<xs:element name="profile" type="gbcs-client:profileType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="profileType">
<xs:choice>
<xs:element name="basic-auth" type="gbcs-client:basicAuthType"/>
<xs:element name="tls-client-auth" type="gbcs-client:tlsClientAuthType"/>
</xs:choice>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="base-url" type="xs:anyURI" use="required"/>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
</xs:complexType>
<xs:complexType name="basicAuthType">
<xs:attribute name="user" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tlsClientAuthType">
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/>
<xs:attribute name="key-store-password" type="xs:string" use="required"/>
<xs:attribute name="key-alias" type="xs:token" use="required"/>
<xs:attribute name="key-password" type="xs:string" use="optional"/>
</xs:complexType>
</xs:schema>

View File

@@ -1,16 +0,0 @@
<?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

@@ -1,14 +0,0 @@
import net.woggioni.gbcs.api.CacheProvider;
module net.woggioni.gbcs.memcached {
requires net.woggioni.gbcs.base;
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.memcached.MemcachedCacheProvider;
opens net.woggioni.gbcs.memcached.schema;
}

View File

@@ -1,59 +0,0 @@
package net.woggioni.gbcs.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.base.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

@@ -1,26 +0,0 @@
package net.woggioni.gbcs.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.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-memcached"
override fun getTypeName() = "memcachedCacheType"
}

View File

@@ -1,88 +0,0 @@
package net.woggioni.gbcs.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.HostAndPort
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.Xml.Companion.asIterable
import net.woggioni.gbcs.base.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.time.Duration
class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd"
override fun getXmlType() = "memcachedCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.gbcs-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

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

View File

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

View File

@@ -2,9 +2,10 @@ org.gradle.configuration-cache=false
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
gbcs.version = 0.0.1 rbcs.version = 0.3.0-SNAPSHOT
lys.version = 2025.01.10 lys.version = 2025.06.10
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

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum

View File

@@ -5,6 +5,12 @@ plugins {
} }
dependencies { dependencies {
implementation catalog.slf4j.api
implementation project(':rbcs-common')
api catalog.netty.common
api catalog.netty.buffer
api catalog.netty.handler
api catalog.netty.codec.http
} }
publishing { publishing {

View File

@@ -0,0 +1,16 @@
module net.woggioni.rbcs.api {
requires static lombok;
requires io.netty.handler;
requires io.netty.common;
requires net.woggioni.rbcs.common;
requires io.netty.transport;
requires io.netty.codec.http;
requires io.netty.buffer;
requires org.slf4j;
requires java.xml;
exports net.woggioni.rbcs.api;
exports net.woggioni.rbcs.api.exception;
exports net.woggioni.rbcs.api.message;
}

View File

@@ -0,0 +1,13 @@
package net.woggioni.rbcs.api;
import java.util.concurrent.CompletableFuture;
public interface AsyncCloseable extends AutoCloseable {
CompletableFuture<Void> asyncClose();
@Override
default void close() throws Exception {
asyncClose().get();
}
}

View File

@@ -0,0 +1,57 @@
package net.woggioni.rbcs.api;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.ReferenceCounted;
import lombok.extern.slf4j.Slf4j;
import net.woggioni.rbcs.api.message.CacheMessage;
@Slf4j
public abstract class CacheHandler extends ChannelInboundHandlerAdapter {
private boolean requestFinished = false;
abstract protected void channelRead0(ChannelHandlerContext ctx, CacheMessage msg);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if(!requestFinished && msg instanceof CacheMessage) {
if(msg instanceof CacheMessage.LastCacheContent) requestFinished = true;
try {
channelRead0(ctx, (CacheMessage) msg);
} finally {
if(msg instanceof ReferenceCounted rc) rc.release();
}
} else {
ctx.fireChannelRead(msg);
}
}
protected void sendMessageAndFlush(ChannelHandlerContext ctx, Object msg) {
sendMessage(ctx, msg, true);
}
protected void sendMessage(ChannelHandlerContext ctx, Object msg) {
sendMessage(ctx, msg, false);
}
private void sendMessage(ChannelHandlerContext ctx, Object msg, boolean flush) {
ctx.write(msg);
if(
msg instanceof CacheMessage.LastCacheContent ||
msg instanceof CacheMessage.CachePutResponse ||
msg instanceof CacheMessage.CacheValueNotFoundResponse ||
msg instanceof LastHttpContent
) {
ctx.flush();
ctx.pipeline().remove(this);
} else if(flush) {
ctx.flush();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}

View File

@@ -0,0 +1,15 @@
package net.woggioni.rbcs.api;
import io.netty.channel.ChannelFactory;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketChannel;
public interface CacheHandlerFactory extends AsyncCloseable {
CacheHandler newHandler(
Configuration configuration,
EventLoopGroup eventLoopGroup,
ChannelFactory<SocketChannel> socketChannelFactory,
ChannelFactory<DatagramChannel> datagramChannelFactory
);
}

View File

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

View File

@@ -0,0 +1,13 @@
package net.woggioni.rbcs.api;
import java.io.Serializable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class CacheValueMetadata implements Serializable {
private final String contentDisposition;
private final String mimeType;
}

View File

@@ -1,32 +1,61 @@
package net.woggioni.gbcs.api; package net.woggioni.rbcs.api;
import lombok.EqualsAndHashCode;
import lombok.Value;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Value;
@Value @Value
public class Configuration { public class Configuration {
String host; String host;
int port; int port;
int incomingConnectionsBacklogSize;
String serverPath; String serverPath;
@NonNull
EventExecutor eventExecutor;
@NonNull
Connection connection;
Map<String, User> users; Map<String, User> users;
Map<String, Group> groups; Map<String, Group> groups;
Cache cache; Cache cache;
Authentication authentication; Authentication authentication;
Tls tls; Tls tls;
boolean useVirtualThread;
@Value
public static class EventExecutor {
boolean useVirtualThreads;
}
@Value
public static class Connection {
Duration idleTimeout;
Duration readIdleTimeout;
Duration writeIdleTimeout;
int maxRequestSize;
int chunkSize;
}
@Value
public static class Quota {
long calls;
Duration period;
long initialAvailableCalls;
long maxAvailableCalls;
}
@Value @Value
public static class Group { public static class Group {
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
String name; String name;
Set<Role> roles; Set<Role> roles;
Quota groupQuota;
Quota userQuota;
} }
@Value @Value
@@ -35,7 +64,7 @@ public class Configuration {
String name; String name;
String password; String password;
Set<Group> groups; Set<Group> groups;
Quota quota;
public Set<Role> getRoles() { public Set<Role> getRoles() {
return groups.stream() return groups.stream()
@@ -58,7 +87,6 @@ public class Configuration {
public static class Tls { public static class Tls {
KeyStore keyStore; KeyStore keyStore;
TrustStore trustStore; TrustStore trustStore;
boolean verifyClients;
} }
@Value @Value
@@ -74,6 +102,7 @@ public class Configuration {
Path file; Path file;
String password; String password;
boolean checkCertificateStatus; boolean checkCertificateStatus;
boolean requireClientCertificate;
} }
@Value @Value
@@ -93,7 +122,7 @@ public class Configuration {
} }
public interface Cache { public interface Cache {
net.woggioni.gbcs.api.Cache materialize(); CacheHandlerFactory materialize();
String getNamespaceURI(); String getNamespaceURI();
String getTypeName(); String getTypeName();
} }
@@ -101,24 +130,28 @@ public class Configuration {
public static Configuration of( public static Configuration of(
String host, String host,
int port, int port,
int incomingConnectionsBacklogSize,
String serverPath, String serverPath,
EventExecutor eventExecutor,
Connection connection,
Map<String, User> users, Map<String, User> users,
Map<String, Group> groups, Map<String, Group> groups,
Cache cache, Cache cache,
Authentication authentication, Authentication authentication,
Tls tls, Tls tls
boolean useVirtualThread
) { ) {
return new Configuration( return new Configuration(
host, host,
port, port,
incomingConnectionsBacklogSize,
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null, serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
eventExecutor,
connection,
users, users,
groups, groups,
cache, cache,
authentication, authentication,
tls, tls
useVirtualThread
); );
} }
} }

View File

@@ -0,0 +1,5 @@
package net.woggioni.rbcs.api;
public enum Role {
Reader, Writer, Healthcheck
}

View File

@@ -0,0 +1,11 @@
package net.woggioni.rbcs.api.exception;
public class CacheException extends RbcsException {
public CacheException(String message, Throwable cause) {
super(message, cause);
}
public CacheException(String message) {
this(message, null);
}
}

View File

@@ -1,6 +1,6 @@
package net.woggioni.gbcs.api.exception; package net.woggioni.rbcs.api.exception;
public class ConfigurationException extends GbcsException { public class ConfigurationException extends RbcsException {
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.gbcs.api.exception; package net.woggioni.rbcs.api.exception;
public class ContentTooLargeException extends GbcsException { public class ContentTooLargeException extends RbcsException {
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.rbcs.api.exception;
public class RbcsException extends RuntimeException {
public RbcsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,161 @@
package net.woggioni.rbcs.api.message;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.woggioni.rbcs.api.CacheValueMetadata;
public sealed interface CacheMessage {
@Getter
@RequiredArgsConstructor
final class CacheGetRequest implements CacheMessage {
private final String key;
}
abstract sealed class CacheGetResponse implements CacheMessage {
}
@Getter
@RequiredArgsConstructor
final class CacheValueFoundResponse extends CacheGetResponse {
private final String key;
private final CacheValueMetadata metadata;
}
final class CacheValueNotFoundResponse extends CacheGetResponse {
}
@Getter
@RequiredArgsConstructor
final class CachePutRequest implements CacheMessage {
private final String key;
private final CacheValueMetadata metadata;
}
@Getter
@RequiredArgsConstructor
final class CachePutResponse implements CacheMessage {
private final String key;
}
@RequiredArgsConstructor
non-sealed class CacheContent implements CacheMessage, ByteBufHolder {
protected final ByteBuf chunk;
@Override
public ByteBuf content() {
return chunk;
}
@Override
public CacheContent copy() {
return replace(chunk.copy());
}
@Override
public CacheContent duplicate() {
return new CacheContent(chunk.duplicate());
}
@Override
public CacheContent retainedDuplicate() {
return new CacheContent(chunk.retainedDuplicate());
}
@Override
public CacheContent replace(ByteBuf content) {
return new CacheContent(content);
}
@Override
public CacheContent retain() {
chunk.retain();
return this;
}
@Override
public CacheContent retain(int increment) {
chunk.retain(increment);
return this;
}
@Override
public CacheContent touch() {
chunk.touch();
return this;
}
@Override
public CacheContent touch(Object hint) {
chunk.touch(hint);
return this;
}
@Override
public int refCnt() {
return chunk.refCnt();
}
@Override
public boolean release() {
return chunk.release();
}
@Override
public boolean release(int decrement) {
return chunk.release(decrement);
}
}
final class LastCacheContent extends CacheContent {
public LastCacheContent(ByteBuf chunk) {
super(chunk);
}
@Override
public LastCacheContent copy() {
return replace(chunk.copy());
}
@Override
public LastCacheContent duplicate() {
return new LastCacheContent(chunk.duplicate());
}
@Override
public LastCacheContent retainedDuplicate() {
return new LastCacheContent(chunk.retainedDuplicate());
}
@Override
public LastCacheContent replace(ByteBuf content) {
return new LastCacheContent(chunk);
}
@Override
public LastCacheContent retain() {
super.retain();
return this;
}
@Override
public LastCacheContent retain(int increment) {
super.retain(increment);
return this;
}
@Override
public LastCacheContent touch() {
super.touch();
return this;
}
@Override
public LastCacheContent touch(Object hint) {
super.touch(hint);
return this;
}
}
}

184
rbcs-cli/build.gradle Normal file
View File

@@ -0,0 +1,184 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
alias catalog.plugins.envelope
alias catalog.plugins.sambal
alias catalog.plugins.graalvm.native.image
alias catalog.plugins.graalvm.jlink
alias catalog.plugins.jpms.check
id 'maven-publish'
}
import net.woggioni.gradle.envelope.EnvelopePlugin
import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
import net.woggioni.gradle.graalvm.NativeImageTask
import net.woggioni.gradle.graalvm.NativeImagePlugin
import net.woggioni.gradle.graalvm.UpxTask
import net.woggioni.gradle.graalvm.JlinkPlugin
import net.woggioni.gradle.graalvm.JlinkTask
sourceSets {
configureNativeImage {
java {
}
kotlin {
}
}
}
configurations {
release {
transitive = false
canBeConsumed = true
canBeResolved = true
visible = true
}
configureNativeImageImplementation {
extendsFrom implementation
}
configureNativeImageRuntimeOnly {
extendsFrom runtimeOnly
}
nativeImage {
extendsFrom runtimeClasspath
}
}
dependencies {
configureNativeImageImplementation project
configureNativeImageImplementation project(':rbcs-server-memcache')
implementation catalog.jwo
implementation catalog.slf4j.api
implementation catalog.picocli
implementation project(':rbcs-client')
implementation project(':rbcs-server')
// runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic
// runtimeOnly catalog.slf4j.simple
nativeImage project(':rbcs-server-memcache')
}
Property<String> mainModuleName = objects.property(String.class)
mainModuleName.set('net.woggioni.rbcs.cli')
Property<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.rbcs.cli.RemoteBuildCacheServerCli')
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.javaModuleMainClass = mainClassName
}
Provider<Jar> jarTaskProvider = tasks.named(JavaPlugin.JAR_TASK_NAME, Jar)
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.ENVELOPE_JAR_TASK_NAME, EnvelopeJarTask.class) {
mainModule = mainModuleName
mainClass = mainClassName
extraClasspath = ["plugins"]
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/rbcs/cli/logback.xml'
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED'
}
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = "net.woggioni.rbcs.cli.graal.GraalNativeImageConfiguration"
classpath = project.files(
configurations.configureNativeImageRuntimeClasspath,
sourceSets.configureNativeImage.output
)
mergeConfiguration = false
systemProperty('logback.configurationFile', 'classpath:net/woggioni/rbcs/cli/logback.xml')
systemProperty('io.netty.leakDetectionLevel', 'DISABLED')
modularity.inferModulePath = false
enabled = true
systemProperty('gradle.tmp.dir', temporaryDir.toString())
}
nativeImage {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = mainClassName
// mainModule = mainModuleName
useMusl = true
buildStaticImage = true
linkAtBuildTime = false
classpath = project.files(jarTaskProvider, configurations.nativeImage)
compressExecutable = true
compressionLevel = 6
useLZMA = false
}
Provider<UpxTask> upxTaskProvider = tasks.named(NativeImagePlugin.UPX_TASK_NAME, UpxTask) {
}
Provider<JlinkTask> jlinkTaskProvider = tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = mainClassName
mainModule = 'net.woggioni.rbcs.cli'
classpath = project.files(
configurations.configureNativeImageRuntimeClasspath,
sourceSets.configureNativeImage.output
)
additionalModules = [
'net.woggioni.rbcs.server.memcache',
'ch.qos.logback.classic',
'jdk.crypto.ec'
]
compressionLevel = 2
stripDebug = false
}
Provider<Tar> jlinkDistTarTaskProvider = tasks.named(JlinkPlugin.JLINK_DIST_TAR_TASK_NAME, Tar) {
exclude 'lib/libjvmcicompiler.so'
}
tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) {
from(rootProject.file('conf')) {
into('net/woggioni/rbcs/cli')
include 'logback.xml'
include 'logging.properties'
}
}
artifacts {
release(envelopeJarTaskProvider)
release(upxTaskProvider)
release(jlinkDistTarTaskProvider)
}
publishing {
publications {
maven(MavenPublication) {
artifact envelopeJar
artifact(upxTaskProvider) {
classifier = "linux-x86_64"
extension = "exe"
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs-client="urn:net.woggioni.rbcs.client"
xs:schemaLocation="urn:net.woggioni.rbcs.client jpms://net.woggioni.rbcs.client/net/woggioni/rbcs/client/schema/rbcs-client.xsd"
>
<profile name="profile1" base-url="https://rbcs1.example.com/">
<no-auth/>
<connection write-idle-timeout="PT60S"
read-idle-timeout="PT60S"
idle-timeout="PT30S" />
</profile>
<profile name="profile2" base-url="https://rbcs2.example.com/">
<basic-auth user="user" password="password"/>
</profile>
</rbcs-client:profiles>

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

@@ -0,0 +1,6 @@
[
{
"name":"java.lang.Boolean",
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
}
]

View File

@@ -0,0 +1,2 @@
Args=-O3 -march=x86-64-v2 --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

View File

@@ -0,0 +1,8 @@
[
{
"type":"agent-extracted",
"classes":[
]
}
]

View File

@@ -0,0 +1,2 @@
[
]

View File

@@ -0,0 +1,728 @@
[
{
"name":"android.os.Build$VERSION"
},
{
"name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.joran.SerializedModelConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.util.DefaultJoranConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.ConsoleAppender",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"setTarget","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.OutputStreamAppender",
"methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }]
},
{
"name":"ch.qos.logback.core.encoder.Encoder",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder",
"methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }]
},
{
"name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase",
"methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.spi.ContextAware",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"com.aayushatharva.brotli4j.Brotli4jLoader"
},
{
"name":"com.github.luben.zstd.Zstd"
},
{
"name":"com.sun.crypto.provider.AESCipher$General",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.ARCFOURCipher",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.DESCipher",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.DESedeCipher",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.DHParameters",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.HmacCore$HmacSHA512",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA512",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.crypto.provider.TlsMasterSecretGenerator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.impl.dv.xs.ExtendedSchemaDVFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.impl.dv.xs.SchemaDVFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"groovy.lang.Closure"
},
{
"name":"io.netty.bootstrap.ServerBootstrap$1"
},
{
"name":"io.netty.bootstrap.ServerBootstrap$ServerBootstrapAcceptor",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"io.netty.buffer.AbstractByteBufAllocator",
"queryAllDeclaredMethods":true
},
{
"name":"io.netty.buffer.AbstractReferenceCountedByteBuf",
"fields":[{"name":"refCnt"}]
},
{
"name":"io.netty.buffer.AdaptivePoolingAllocator$Chunk",
"fields":[{"name":"refCnt"}]
},
{
"name":"io.netty.buffer.AdaptivePoolingAllocator$Magazine",
"fields":[{"name":"nextInLine"}]
},
{
"name":"io.netty.channel.AbstractChannelHandlerContext",
"fields":[{"name":"handlerState"}]
},
{
"name":"io.netty.channel.ChannelDuplexHandler",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.channel.ChannelHandlerAdapter",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"io.netty.channel.ChannelInboundHandlerAdapter",
"methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.channel.ChannelInitializer",
"methods":[{"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"io.netty.channel.ChannelOutboundBuffer",
"fields":[{"name":"totalPendingSize"}, {"name":"unwritable"}]
},
{
"name":"io.netty.channel.ChannelOutboundHandlerAdapter",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.channel.CombinedChannelDuplexHandler",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.channel.DefaultChannelConfig",
"fields":[{"name":"autoRead"}, {"name":"writeBufferWaterMark"}]
},
{
"name":"io.netty.channel.DefaultChannelPipeline",
"fields":[{"name":"estimatorHandle"}]
},
{
"name":"io.netty.channel.DefaultChannelPipeline$HeadContext",
"methods":[{"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.channel.DefaultChannelPipeline$TailContext",
"methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.channel.SimpleChannelInboundHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.channel.embedded.EmbeddedChannel$2"
},
{
"name":"io.netty.channel.pool.SimpleChannelPool$1"
},
{
"name":"io.netty.channel.socket.nio.NioSocketChannel",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"io.netty.handler.codec.ByteToMessageDecoder",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.handler.codec.MessageAggregator",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.MessageToByteEncoder",
"methods":[{"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.handler.codec.MessageToMessageCodec",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.MessageToMessageDecoder",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"io.netty.handler.codec.compression.JdkZlibDecoder"
},
{
"name":"io.netty.handler.codec.compression.JdkZlibEncoder",
"methods":[{"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.handler.codec.http.HttpClientCodec"
},
{
"name":"io.netty.handler.codec.http.HttpContentDecoder",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.http.HttpContentDecompressor"
},
{
"name":"io.netty.handler.codec.http.HttpContentEncoder",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }]
},
{
"name":"io.netty.handler.codec.http.HttpObjectAggregator"
},
{
"name":"io.netty.handler.codec.http.HttpServerCodec"
},
{
"name":"io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec"
},
{
"name":"io.netty.handler.stream.ChunkedWriteHandler",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.handler.timeout.IdleStateHandler",
"methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"io.netty.internal.tcnative.SSLContext"
},
{
"name":"io.netty.util.AbstractReferenceCounted",
"fields":[{"name":"refCnt"}]
},
{
"name":"io.netty.util.DefaultAttributeMap",
"fields":[{"name":"attributes"}]
},
{
"name":"io.netty.util.DefaultAttributeMap$DefaultAttribute",
"fields":[{"name":"attributeMap"}]
},
{
"name":"io.netty.util.Recycler$DefaultHandle",
"fields":[{"name":"state"}]
},
{
"name":"io.netty.util.ReferenceCountUtil",
"queryAllDeclaredMethods":true
},
{
"name":"io.netty.util.concurrent.DefaultPromise",
"fields":[{"name":"result"}]
},
{
"name":"io.netty.util.concurrent.SingleThreadEventExecutor",
"fields":[{"name":"state"}, {"name":"threadProperties"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields",
"fields":[{"name":"producerLimit"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields",
"fields":[{"name":"consumerIndex"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields",
"fields":[{"name":"producerIndex"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.MpmcArrayQueueConsumerIndexField",
"fields":[{"name":"consumerIndex"}]
},
{
"name":"io.netty.util.internal.shaded.org.jctools.queues.MpmcArrayQueueProducerIndexField",
"fields":[{"name":"producerIndex"}]
},
{
"name":"java.lang.Object",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.lang.ProcessHandle",
"methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }]
},
{
"name":"java.lang.System",
"methods":[{"name":"console","parameterTypes":[] }]
},
{
"name":"java.lang.Thread",
"fields":[{"name":"threadLocalRandomProbe"}],
"methods":[{"name":"isVirtual","parameterTypes":[] }]
},
{
"name":"java.nio.Bits",
"fields":[{"name":"MAX_MEMORY"}, {"name":"UNALIGNED"}]
},
{
"name":"java.nio.Buffer",
"fields":[{"name":"address"}]
},
{
"name":"java.nio.ByteBuffer",
"methods":[{"name":"alignedSlice","parameterTypes":["int"] }]
},
{
"name":"java.nio.DirectByteBuffer",
"methods":[{"name":"<init>","parameterTypes":["long","long"] }]
},
{
"name":"java.nio.channels.spi.SelectorProvider",
"methods":[{"name":"openServerSocketChannel","parameterTypes":["java.net.ProtocolFamily"] }, {"name":"openSocketChannel","parameterTypes":["java.net.ProtocolFamily"] }]
},
{
"name":"java.nio.file.Path"
},
{
"name":"java.nio.file.Paths",
"methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }]
},
{
"name":"java.security.AlgorithmParametersSpi"
},
{
"name":"java.security.KeyStoreSpi"
},
{
"name":"java.security.SecureRandomParameters"
},
{
"name":"java.sql.Connection"
},
{
"name":"java.sql.Driver"
},
{
"name":"java.sql.DriverManager",
"methods":[{"name":"getConnection","parameterTypes":["java.lang.String"] }, {"name":"getDriver","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.sql.Time",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"java.sql.Timestamp",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.Duration",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Instant",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDate",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.MonthDay",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Period",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Year",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.YearMonth",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.ZoneId",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZoneOffset",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZonedDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.util.concurrent.ForkJoinTask",
"fields":[{"name":"aux"}, {"name":"status"}]
},
{
"name":"java.util.concurrent.atomic.AtomicBoolean",
"fields":[{"name":"value"}]
},
{
"name":"java.util.concurrent.atomic.AtomicReference",
"fields":[{"name":"value"}]
},
{
"name":"java.util.concurrent.atomic.Striped64",
"fields":[{"name":"base"}, {"name":"cellsBusy"}]
},
{
"name":"java.util.concurrent.atomic.Striped64$Cell",
"fields":[{"name":"value"}]
},
{
"name":"javax.security.auth.x500.X500Principal",
"fields":[{"name":"thisX500Name"}],
"methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }]
},
{
"name":"jdk.internal.misc.Unsafe",
"methods":[{"name":"getUnsafe","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.api.CacheHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.cli.RemoteBuildCacheServerCli",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.RemoteBuildCacheServerCli$VersionProvider",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.cli.impl.RbcsCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.ClientCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.GetCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.PasswordHashCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.PutCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.commands.ServerCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"net.woggioni.rbcs.cli.impl.converters.ByteSizeConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.cli.impl.converters.DurationConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"net.woggioni.rbcs.client.RemoteBuildCacheClient$sendRequest$1$operationComplete$responseHandler$1",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$HttpChunkContentCompressor",
"methods":[{"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$NettyHttpBasicAuthenticator"
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$ServerInitializer"
},
{
"name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$ServerInitializer$initChannel$4",
"methods":[{"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.cache.FileSystemCacheHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.cache.InMemoryCacheHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.exception.ExceptionHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.BlackHoleRequestHandler"
},
{
"name":"net.woggioni.rbcs.server.handler.MaxRequestSizeHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.ServerHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{
"name":"net.woggioni.rbcs.server.handler.TraceHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.memcache.MemcacheCacheHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.memcache.client.MemcacheClient$sendRequest$1$operationComplete$handler$1",
"methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
},
{
"name":"net.woggioni.rbcs.server.throttling.ThrottlingHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"sun.misc.Unsafe",
"fields":[{"name":"theUnsafe"}],
"methods":[{"name":"copyMemory","parameterTypes":["java.lang.Object","long","java.lang.Object","long","long"] }, {"name":"getAndAddLong","parameterTypes":["java.lang.Object","long","long"] }, {"name":"getAndSetObject","parameterTypes":["java.lang.Object","long","java.lang.Object"] }, {"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }]
},
{
"name":"sun.nio.ch.SelectorImpl",
"fields":[{"name":"publicSelectedKeys"}, {"name":"selectedKeys"}]
},
{
"name":"sun.security.pkcs12.PKCS12KeyStore",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.DSA$SHA224withDSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.DSA$SHA256withDSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.MD5",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.NativePRNG",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.security.SecureRandomParameters"] }]
},
{
"name":"sun.security.provider.NativePRNG$NonBlocking",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.security.SecureRandomParameters"] }]
},
{
"name":"sun.security.provider.SHA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA2$SHA224",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA2$SHA256",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA5$SHA384",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.SHA5$SHA512",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.X509Factory",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.PSSParameters",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.RSAKeyFactory$Legacy",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.RSAPSSSignature",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.rsa.RSASignature$SHA224withRSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.KeyManagerFactoryImpl$SunX509",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.SSLContextImpl$DefaultSSLContext",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.SSLContextImpl$TLSContext",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.x509.AuthorityInfoAccessExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.AuthorityKeyIdentifierExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.BasicConstraintsExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.CRLDistributionPointsExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.CertificatePoliciesExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.ExtendedKeyUsageExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.IssuerAlternativeNameExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.KeyUsageExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.NetscapeCertTypeExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.PrivateKeyUsageExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.SubjectAlternativeNameExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
},
{
"name":"sun.security.x509.SubjectKeyIdentifierExtension",
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]
}
]

View File

@@ -0,0 +1,46 @@
{
"resources":{
"includes":[{
"pattern":"\\QMETA-INF/MANIFEST.MF\\E"
}, {
"pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E"
}, {
"pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E"
}, {
"pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/javax.xml.parsers.DocumentBuilderFactory\\E"
}, {
"pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E"
}, {
"pattern":"\\QMETA-INF/services/net.woggioni.rbcs.api.CacheProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
}, {
"pattern":"\\Qclasspath:net/woggioni/rbcs/cli/logback.xml\\E"
}, {
"pattern":"\\Qlogback-test.scmo\\E"
}, {
"pattern":"\\Qlogback.scmo\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/cli/logback.xml\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/client/schema/rbcs-client.xsd\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/server/rbcs-default.xml\\E"
}, {
"pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E"
}]},
"bundles":[{
"name":"com.sun.org.apache.xerces.internal.impl.xpath.regex.message",
"locales":[""]
}]
}

View File

@@ -0,0 +1,14 @@
{
"types":[
{
"name":"java.lang.String"
},
{
"name":"net.woggioni.rbcs.api.CacheValueMetadata"
}
],
"lambdaCapturingTypes":[
],
"proxies":[
]
}

View File

@@ -0,0 +1,186 @@
package net.woggioni.rbcs.cli.graal
import net.woggioni.jwo.NullOutputStream
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Configuration.User
import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.cli.RemoteBuildCacheServerCli
import net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand
import net.woggioni.rbcs.cli.impl.commands.GetCommand
import net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand
import net.woggioni.rbcs.cli.impl.commands.PutCommand
import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import net.woggioni.rbcs.common.RBCS
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.RemoteBuildCacheServer
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.configuration.Parser
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
import java.io.ByteArrayInputStream
import java.net.URI
import java.nio.file.Path
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.ExecutionException
import java.util.zip.Deflater
import net.woggioni.rbcs.client.Configuration as ClientConfiguration
import net.woggioni.rbcs.client.impl.Parser as ClientConfigurationParser
object GraalNativeImageConfiguration {
@JvmStatic
fun main(vararg args : String) {
val serverURL = URI.create("file:conf/rbcs-server.xml").toURL()
val serverDoc = serverURL.openStream().use {
Xml.parseXml(serverURL, it)
}
Parser.parse(serverDoc)
val url = URI.create("file:conf/rbcs-client.xml").toURL()
val clientDoc = url.openStream().use {
Xml.parseXml(url, it)
}
ClientConfigurationParser.parse(clientDoc)
val PASSWORD = "password"
val readersGroup = Configuration.Group("readers", setOf(Role.Reader, Role.Healthcheck), null, null)
val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null, null)
val users = listOf(
User("user1", hashPassword(PASSWORD), setOf(readersGroup), null),
User("user2", hashPassword(PASSWORD), setOf(writersGroup), null),
User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup), null),
User("", null, setOf(readersGroup), null),
User("user4", hashPassword(PASSWORD), setOf(readersGroup),
Configuration.Quota(1, Duration.of(1, ChronoUnit.DAYS), 0, 1)
),
User("user5", hashPassword(PASSWORD), setOf(readersGroup),
Configuration.Quota(1, Duration.of(5, ChronoUnit.SECONDS), 0, 1)
)
)
val serverPort = RBCS.getFreePort()
val caches = listOf<Configuration.Cache>(
InMemoryCacheConfiguration(
maxAge = Duration.ofSeconds(3600),
digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false,
maxSize = 0x1000000,
),
FileSystemCacheConfiguration(
Path.of(System.getProperty("java.io.tmpdir")).resolve("rbcs"),
maxAge = Duration.ofSeconds(3600),
digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false,
),
MemcacheCacheConfiguration(
listOf(MemcacheCacheConfiguration.Server(
HostAndPort("127.0.0.1", 11211),
1000,
4)
),
Duration.ofSeconds(60),
"MD5",
null,
1,
)
)
for (cache in caches) {
val serverConfiguration = Configuration(
"127.0.0.1",
serverPort,
100,
null,
Configuration.EventExecutor(true),
Configuration.Connection(
Duration.ofSeconds(10),
Duration.ofSeconds(15),
Duration.ofSeconds(15),
0x10000,
0x1000
),
users.asSequence().map { it.name to it }.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
cache,
Configuration.BasicAuthentication(),
null,
)
MemcacheCacheConfiguration(
listOf(
MemcacheCacheConfiguration.Server(
HostAndPort("127.0.0.1", 11211),
1000,
4
)
),
Duration.ofSeconds(60),
"MD5",
null,
1,
)
val serverHandle = RemoteBuildCacheServer(serverConfiguration).run()
val clientProfile = ClientConfiguration.Profile(
URI.create("http://127.0.0.1:$serverPort/"),
ClientConfiguration.Connection(
Duration.ofSeconds(5),
Duration.ofSeconds(5),
Duration.ofSeconds(7),
true,
),
ClientConfiguration.Authentication.BasicAuthenticationCredentials("user3", PASSWORD),
Duration.ofSeconds(3),
10,
true,
ClientConfiguration.RetryPolicy(
3,
1000,
1.2
),
ClientConfiguration.TrustStore(null, null, false, false)
)
HealthCheckCommand.execute(clientProfile)
BenchmarkCommand.execute(
clientProfile,
1000,
0x100,
true
)
PutCommand.execute(
clientProfile,
"some-file.bin",
ByteArrayInputStream(ByteArray(0x1000) { it.toByte() }),
"application/octet-setream",
"attachment; filename=\"some-file.bin\""
)
GetCommand.execute(
clientProfile,
"some-file.bin",
NullOutputStream()
)
serverHandle.sendShutdownSignal()
try {
serverHandle.get()
} catch (ee : ExecutionException) {
}
}
System.setProperty("net.woggioni.rbcs.conf.dir", System.getProperty("gradle.tmp.dir"))
RemoteBuildCacheServerCli.createCommandLine().execute("--version")
RemoteBuildCacheServerCli.createCommandLine().execute("server", "-t", "PT10S")
}
}

View File

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

View File

@@ -0,0 +1,79 @@
package net.woggioni.rbcs.cli
import net.woggioni.jwo.Application
import net.woggioni.rbcs.cli.impl.AbstractVersionProvider
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand
import net.woggioni.rbcs.cli.impl.commands.ClientCommand
import net.woggioni.rbcs.cli.impl.commands.GetCommand
import net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand
import net.woggioni.rbcs.cli.impl.commands.PasswordHashCommand
import net.woggioni.rbcs.cli.impl.commands.PutCommand
import net.woggioni.rbcs.cli.impl.commands.ServerCommand
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine
import picocli.CommandLine.Model.CommandSpec
@CommandLine.Command(
name = "rbcs", versionProvider = RemoteBuildCacheServerCli.VersionProvider::class
)
class RemoteBuildCacheServerCli : RbcsCommand() {
class VersionProvider : AbstractVersionProvider()
companion object {
private fun setPropertyIfNotPresent(key: String, value: String) {
System.getProperty(key) ?: System.setProperty(key, value)
}
fun createCommandLine() : CommandLine {
setPropertyIfNotPresent("logback.configurationFile", "net/woggioni/rbcs/cli/logback.xml")
setPropertyIfNotPresent("io.netty.leakDetectionLevel", "DISABLED")
val currentClassLoader = RemoteBuildCacheServerCli::class.java.classLoader
Thread.currentThread().contextClassLoader = currentClassLoader
if(currentClassLoader.javaClass.name == "net.woggioni.envelope.loader.ModuleClassLoader") {
//We're running in an envelope jar and custom URL protocols won't work
RbcsUrlStreamHandlerFactory.install()
}
val log = createLogger<RemoteBuildCacheServerCli>()
val app = Application.builder("rbcs")
.configurationDirectoryEnvVar("RBCS_CONFIGURATION_DIR")
.configurationDirectoryPropertyKey("net.woggioni.rbcs.conf.dir")
.build()
val rbcsCli = RemoteBuildCacheServerCli()
val commandLine = CommandLine(rbcsCli)
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())
addSubcommand(HealthCheckCommand())
})
return commandLine
}
@JvmStatic
fun main(vararg args: String) {
System.exit(createCommandLine().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.gbcs.cli.impl package net.woggioni.rbcs.cli.impl
import picocli.CommandLine import picocli.CommandLine
import java.util.jar.Attributes import java.util.jar.Attributes

View File

@@ -1,18 +1,18 @@
package net.woggioni.gbcs.cli.impl package net.woggioni.rbcs.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 GbcsCommand : Runnable { abstract class RbcsCommand : Runnable {
@CommandLine.Option(names = ["-h", "--help"], usageHelp = true) @CommandLine.Option(names = ["-h", "--help"], usageHelp = true)
var usageHelp = false var usageHelp = false
private set private set
protected fun findConfigurationFile(app: Application, fileName : String): Path { protected fun findConfigurationFile(app: Application, fileName : String): Path {
val confDir = app.computeConfigurationDirectory() val confDir = app.computeConfigurationDirectory(false)
val configurationFile = confDir.resolve(fileName) val configurationFile = confDir.resolve(fileName)
return configurationFile return configurationFile
} }

View File

@@ -0,0 +1,186 @@
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.jwo.JWO
import net.woggioni.jwo.LongMath
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.cli.impl.converters.ByteSizeConverter
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.error
import net.woggioni.rbcs.common.info
import picocli.CommandLine
import java.security.SecureRandom
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
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 : RbcsCommand() {
companion object {
private val log = createLogger<BenchmarkCommand>()
fun execute(profile : Configuration.Profile,
numberOfEntries : Int,
entrySize : Int,
useRandomValue : Boolean,
) {
val progressThreshold = LongMath.ceilDiv(numberOfEntries.toLong(), 20)
RemoteBuildCacheClient(profile).use { client ->
val entryGenerator = sequence {
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
while (true) {
val key = JWO.bytesToHex(random.nextBytes(16))
val value = if (useRandomValue) {
random.nextBytes(entrySize)
} else {
val byteValue = random.nextInt().toByte()
ByteArray(entrySize) { _ -> byteValue }
}
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 * 5)
val iterator = entryGenerator.take(numberOfEntries).iterator()
while (completionCounter.get() < numberOfEntries) {
if (iterator.hasNext()) {
val entry = iterator.next()
semaphore.acquire()
val future =
client.put(entry.first, entry.second, CacheValueMetadata(null, null)).thenApply { entry }
future.whenComplete { result, ex ->
if (ex != null) {
log.error(ex.message, ex)
} else {
completionQueue.put(result)
}
semaphore.release()
val completed = completionCounter.incrementAndGet()
if (completed.mod(progressThreshold) == 0L) {
log.debug {
"Inserted $completed / $numberOfEntries"
}
}
}
} else {
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
}
}
val inserted = completionQueue.toList()
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
val opsPerSecond = String.format("%.2f", numberOfEntries.toDouble() / elapsed * 1000)
"Insertion rate: $opsPerSecond ops/s"
}
inserted
}
log.info {
"Inserted ${entries.size} entries"
}
log.info {
"Starting retrieval"
}
if (entries.isNotEmpty()) {
val completionCounter = AtomicLong(0)
val semaphore = Semaphore(profile.maxConnections * 5)
val start = Instant.now()
val it = entries.iterator()
while (completionCounter.get() < entries.size) {
if (it.hasNext()) {
val entry = it.next()
semaphore.acquire()
val future = client.get(entry.first).thenApply {
if (it == null) {
log.error {
"Missing entry for key '${entry.first}'"
}
} else if (!entry.second.contentEquals(it)) {
log.error {
"Retrieved a value different from what was inserted for key '${entry.first}'"
}
}
}
future.whenComplete { _, _ ->
val completed = completionCounter.incrementAndGet()
if (completed.mod(progressThreshold) == 0L) {
log.debug {
"Retrieved $completed / ${entries.size}"
}
}
semaphore.release()
}
} else {
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
}
}
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000)
"Retrieval rate: $opsPerSecond ops/s"
}
} else {
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
}
}
}
}
@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
@CommandLine.Option(
names = ["-s", "--size"],
description = ["Size of a cache value in bytes"],
paramLabel = "SIZE",
converter = [ByteSizeConverter::class]
)
private var size = 0x1000
@CommandLine.Option(
names = ["-r", "--random"],
description = ["Insert completely random byte values"]
)
private var randomValues = false
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")
}
execute(
profile,
numberOfEntries,
size,
randomValues
)
}
}

View File

@@ -1,38 +1,45 @@
package net.woggioni.gbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GbcsClient
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
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 = ["GBCS client"], description = ["RBCS client"],
showDefaultValues = true showDefaultValues = true
) )
class ClientCommand(app : Application) : GbcsCommand() { class ClientCommand(app : Application) : RbcsCommand() {
@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, "gbcs-client.xml") private var configurationFile : Path = findConfigurationFile(app, "rbcs-client.xml")
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--profile"], names = ["-p", "--profile"],
description = ["Name of the client profile to be used"], description = ["Name of the client profile to be used"],
paramLabel = "PROFILE", paramLabel = "PROFILE",
required = true required = false
) )
var profileName : String? = null var profileName : String? = null
get() = field ?: throw IllegalArgumentException("A profile name must be specified using the '-p' command line parameter")
val configuration : GbcsClient.Configuration by lazy { val configuration : Configuration by lazy {
GbcsClient.Configuration.parse(configurationFile) Configuration.parse(configurationFile)
} }
override fun run() { override fun run() {
val log = createLogger<ClientCommand>()
log.debug {
"Using configuration file '$configurationFile'"
}
println("Available profiles:") println("Available profiles:")
configuration.profiles.forEach { (profileName, _) -> configuration.profiles.forEach { (profileName, _) ->
println(profileName) println(profileName)

View File

@@ -0,0 +1,59 @@
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
@CommandLine.Command(
name = "get",
description = ["Fetch a value from the cache with the specified key"],
showDefaultValues = true
)
class GetCommand : RbcsCommand() {
companion object {
private val log = createLogger<GetCommand>()
fun execute(profile : Configuration.Profile, key : String, outputStream: OutputStream) {
RemoteBuildCacheClient(profile).use { client ->
client.get(key).thenApply { value ->
value?.let {
outputStream.use {
it.write(value)
}
} ?: throw NoSuchElementException("No value found for key $key")
}.get()
}
}
}
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
@CommandLine.Option(
names = ["-k", "--key"],
description = ["The key for the new value"],
paramLabel = "KEY"
)
private var key : String = ""
@CommandLine.Option(
names = ["-v", "--value"],
description = ["Path to a file where the retrieved value will be written (defaults to stdout)"],
paramLabel = "VALUE_FILE",
)
private var output : Path? = null
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")
}
execute(profile, key, (output?.let(Files::newOutputStream) ?: System.out))
}
}

View File

@@ -0,0 +1,53 @@
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine
import java.security.SecureRandom
import kotlin.random.Random
@CommandLine.Command(
name = "health",
description = ["Check server health"],
showDefaultValues = true
)
class HealthCheckCommand : RbcsCommand() {
companion object{
private val log = createLogger<HealthCheckCommand>()
fun execute(profile : Configuration.Profile) {
RemoteBuildCacheClient(profile).use { client ->
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
val nonce = ByteArray(0xa0)
random.nextBytes(nonce)
client.healthCheck(nonce).thenApply { value ->
if(value == null) {
throw IllegalStateException("Empty response from server")
}
val offset = value.size - nonce.size
for(i in 0 until nonce.size) {
val a = nonce[i]
val b = value[offset + i]
if(a != b) {
throw IllegalStateException("Server nonce does not match")
}
}
}.get()
}
}
}
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
override fun run() {
val clientCommand = spec.parent().userObject() as ClientCommand
val profile = clientCommand.profileName.let { profileName ->
clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
}
execute(profile)
}
}

View File

@@ -1,9 +1,9 @@
package net.woggioni.gbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter
import net.woggioni.jwo.UncloseableOutputStream import net.woggioni.jwo.UncloseableOutputStream
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import picocli.CommandLine import picocli.CommandLine
import java.io.OutputStream import java.io.OutputStream
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
@@ -12,10 +12,10 @@ import java.io.PrintWriter
@CommandLine.Command( @CommandLine.Command(
name = "password", name = "password",
description = ["Generate a password hash to add to GBCS configuration file"], description = ["Generate a password hash to add to RBCS configuration file"],
showDefaultValues = true showDefaultValues = true
) )
class PasswordHashCommand : GbcsCommand() { class PasswordHashCommand : RbcsCommand() {
@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

@@ -0,0 +1,113 @@
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.jwo.Hash
import net.woggioni.jwo.JWO
import net.woggioni.jwo.NullOutputStream
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
@CommandLine.Command(
name = "put",
description = ["Add or replace a value to the cache with the specified key"],
showDefaultValues = true
)
class PutCommand : RbcsCommand() {
companion object {
private val log = createLogger<PutCommand>()
fun execute(profile: Configuration.Profile,
actualKey : String,
inputStream: InputStream,
mimeType : String?,
contentDisposition: String?
) {
RemoteBuildCacheClient(profile).use { client ->
inputStream.use {
client.put(actualKey, it.readAllBytes(), CacheValueMetadata(contentDisposition, mimeType))
}.get()
println(profile.serverURI.resolve(actualKey))
}
}
}
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
@CommandLine.Option(
names = ["-k", "--key"],
description = ["The key for the new value, randomly generated if omitted"],
paramLabel = "KEY"
)
private var key : String? = null
@CommandLine.Option(
names = ["-i", "--inline"],
description = ["File is to be displayed in the browser"],
paramLabel = "INLINE",
)
private var inline : Boolean = false
@CommandLine.Option(
names = ["-t", "--type"],
description = ["File mime type"],
paramLabel = "MIME_TYPE",
)
private var mimeType : String? = null
@CommandLine.Option(
names = ["-v", "--value"],
description = ["Path to a file containing the value to be added (defaults to stdin)"],
paramLabel = "VALUE_FILE",
)
private var value : Path? = null
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")
}
RemoteBuildCacheClient(profile).use { client ->
val inputStream : InputStream
val mimeType : String?
val contentDisposition : String?
val valuePath = value
val actualKey : String?
if(valuePath != null) {
inputStream = Files.newInputStream(valuePath)
mimeType = this.mimeType ?: Files.probeContentType(valuePath)
contentDisposition = if(inline) {
"inline"
} else {
"attachment; filename=\"${valuePath.fileName}\""
}
actualKey = key ?: let {
val md = Hash.Algorithm.SHA512.newInputStream(Files.newInputStream(valuePath)).use {
JWO.copy(it, NullOutputStream())
it.messageDigest
}
UUID.nameUUIDFromBytes(md.digest()).toString()
}
} else {
inputStream = System.`in`
mimeType = this.mimeType
contentDisposition = if(inline) {
"inline"
} else {
null
}
actualKey = key ?: UUID.randomUUID().toString()
}
execute(profile, actualKey, inputStream, mimeType, contentDisposition)
}
}
}

View File

@@ -0,0 +1,90 @@
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.cli.impl.converters.DurationConverter
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.info
import net.woggioni.rbcs.server.RemoteBuildCacheServer
import net.woggioni.rbcs.server.RemoteBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
import picocli.CommandLine
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.TimeUnit
@CommandLine.Command(
name = "server",
description = ["RBCS server"],
showDefaultValues = true
)
class ServerCommand(app : Application) : RbcsCommand() {
companion object {
private val log = createLogger<ServerCommand>()
}
private fun createDefaultConfigurationFile(configurationFile: Path) {
log.info {
"Creating default configuration file at '$configurationFile'"
}
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
Files.newOutputStream(configurationFile).use { outputStream ->
defaultConfigurationFileResource.openStream().use { inputStream ->
JWO.copy(inputStream, outputStream)
}
}
}
@CommandLine.Option(
names = ["-t", "--timeout"],
description = ["Exit after the specified time"],
paramLabel = "TIMEOUT",
converter = [DurationConverter::class]
)
private var timeout: Duration? = null
@CommandLine.Option(
names = ["-c", "--config-file"],
description = ["Read the application configuration from this file"],
paramLabel = "CONFIG_FILE"
)
private var configurationFile: Path = findConfigurationFile(app, "rbcs-server.xml")
override fun run() {
if (!Files.exists(configurationFile)) {
Files.createDirectories(configurationFile.parent)
createDefaultConfigurationFile(configurationFile)
}
log.debug {
"Using configuration file '$configurationFile'"
}
val configuration = RemoteBuildCacheServer.loadConfiguration(configurationFile)
log.debug {
ByteArrayOutputStream().also {
RemoteBuildCacheServer.dumpConfiguration(configuration, it)
}.let {
"Server configuration:\n${String(it.toByteArray())}"
}
}
val server = RemoteBuildCacheServer(configuration)
val handle = server.run()
val shutdownHook = Thread.ofPlatform().unstarted {
handle.sendShutdownSignal()
try {
handle.get(60, TimeUnit.SECONDS)
} catch (ex : Throwable) {
log.warn(ex.message, ex)
}
}
Runtime.getRuntime().addShutdownHook(shutdownHook)
if(timeout != null) {
Thread.sleep(timeout)
handle.sendShutdownSignal()
}
handle.get()
}
}

View File

@@ -0,0 +1,10 @@
package net.woggioni.rbcs.cli.impl.converters
import picocli.CommandLine
class ByteSizeConverter : CommandLine.ITypeConverter<Int> {
override fun convert(value: String): Int {
return Integer.decode(value)
}
}

View File

@@ -0,0 +1,11 @@
package net.woggioni.rbcs.cli.impl.converters
import picocli.CommandLine
import java.time.Duration
class DurationConverter : CommandLine.ITypeConverter<Duration> {
override fun convert(value: String): Duration {
return Duration.parse(value)
}
}

View File

@@ -0,0 +1,13 @@
package net.woggioni.rbcs.cli.impl.converters
import picocli.CommandLine
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
class InputStreamConverter : CommandLine.ITypeConverter<InputStream> {
override fun convert(value: String): InputStream {
return Files.newInputStream(Paths.get(value))
}
}

View File

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

19
rbcs-client/build.gradle Normal file
View File

@@ -0,0 +1,19 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
}
dependencies {
implementation project(':rbcs-api')
implementation project(':rbcs-common')
implementation catalog.slf4j.api
implementation catalog.netty.buffer
implementation catalog.netty.handler
implementation catalog.netty.transport
implementation catalog.netty.common
implementation catalog.netty.codec.http
testRuntimeOnly catalog.logback.classic
}

View File

@@ -1,4 +1,4 @@
module net.woggioni.gbcs.client { module net.woggioni.rbcs.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.gbcs.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.gbcs.base; requires net.woggioni.rbcs.common;
requires net.woggioni.gbcs.api; requires net.woggioni.rbcs.api;
requires io.netty.codec; requires io.netty.codec;
requires org.slf4j; requires org.slf4j;
exports net.woggioni.gbcs.client; exports net.woggioni.rbcs.client;
opens net.woggioni.gbcs.client.schema; opens net.woggioni.rbcs.client.schema;
} }

View File

@@ -0,0 +1,62 @@
package net.woggioni.rbcs.client
import net.woggioni.rbcs.client.impl.Parser
import net.woggioni.rbcs.common.Xml
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Duration
data class Configuration(
val profiles: Map<String, Profile>
) {
sealed class Authentication {
data class TlsClientAuthenticationCredentials(
val key: PrivateKey,
val certificateChain: Array<X509Certificate>
) : Authentication()
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
}
class TrustStore (
var file: Path?,
var password: String?,
var checkCertificateStatus: Boolean = false,
var verifyServerCertificate: Boolean = true,
)
class RetryPolicy(
val maxAttempts: Int,
val initialDelayMillis: Long,
val exp: Double
)
class Connection(
val readIdleTimeout: Duration,
val writeIdleTimeout: Duration,
val idleTimeout: Duration,
val requestPipelining : Boolean,
)
data class Profile(
val serverURI: URI,
val connection: Connection,
val authentication: Authentication?,
val connectionTimeout: Duration?,
val maxConnections: Int,
val compressionEnabled: Boolean,
val retryPolicy: RetryPolicy?,
val tlsTruststore : TrustStore?
)
companion object {
fun parse(path: Path): Configuration {
return Files.newInputStream(path).use {
Xml.parseXml(path.toUri().toURL(), it)
}.let(Parser::parse)
}
}
}

View File

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

View File

@@ -0,0 +1,434 @@
package net.woggioni.rbcs.client
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPipeline
import io.netty.channel.IoEventLoopGroup
import io.netty.channel.MultiThreadIoEventLoopGroup
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioIoHandler
import io.netty.channel.pool.AbstractChannelPoolHandler
import io.netty.channel.pool.ChannelPool
import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.DecoderException
import io.netty.handler.codec.http.DefaultFullHttpRequest
import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.HttpClientCodec
import io.netty.handler.codec.http.HttpContentDecompressor
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpObjectAggregator
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.handler.timeout.IdleState
import io.netty.handler.timeout.IdleStateEvent
import io.netty.handler.timeout.IdleStateHandler
import io.netty.util.concurrent.Future
import io.netty.util.concurrent.Future as NettyFuture
import io.netty.util.concurrent.GenericFutureListener
import java.io.IOException
import java.net.InetSocketAddress
import java.net.URI
import java.security.cert.X509Certificate
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.random.Random
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.RBCS.loadKeystore
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.trace
class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
companion object {
private val log = createLogger<RemoteBuildCacheClient>()
}
private val group: IoEventLoopGroup
private val sslContext: SslContext
private val pool: ChannelPool
init {
group = MultiThreadIoEventLoopGroup(NioIoHandler.newFactory())
sslContext = SslContextBuilder.forClient().also { builder ->
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
builder.apply {
keyManager(
tlsClientAuthenticationCredentials.key,
*tlsClientAuthenticationCredentials.certificateChain
)
profile.tlsTruststore?.let { trustStore ->
if (!trustStore.verifyServerCertificate) {
trustManager(object : X509TrustManager {
override fun checkClientTrusted(certChain: Array<out X509Certificate>, p1: String?) {
}
override fun checkServerTrusted(certChain: Array<out X509Certificate>, p1: String?) {
}
override fun getAcceptedIssuers() = null
})
} else {
trustStore.file?.let {
val ts = loadKeystore(it, trustStore.password)
val trustManagerFactory: TrustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(ts)
trustManager(trustManagerFactory)
}
}
}
}
}
}.build()
val (scheme, host, port) = profile.serverURI.run {
Triple(
if (scheme == null) "http" else profile.serverURI.scheme,
host,
port.takeIf { it > 0 } ?: if ("https" == scheme.lowercase()) 443 else 80
)
}
val bootstrap = Bootstrap().apply {
group(group)
channel(NioSocketChannel::class.java)
option(ChannelOption.TCP_NODELAY, true)
option(ChannelOption.SO_KEEPALIVE, true)
remoteAddress(InetSocketAddress(host, port))
profile.connectionTimeout?.let {
option(ChannelOption.CONNECT_TIMEOUT_MILLIS, it.toMillis().toInt())
}
}
val channelPoolHandler = object : AbstractChannelPoolHandler() {
@Volatile
private var connectionCount = AtomicInteger()
@Volatile
private var leaseCount = AtomicInteger()
override fun channelReleased(ch: Channel) {
val activeLeases = leaseCount.decrementAndGet()
log.trace {
"Released channel ${ch.id().asShortText()}, number of active leases: $activeLeases"
}
}
override fun channelAcquired(ch: Channel) {
val activeLeases = leaseCount.getAndIncrement()
log.trace {
"Acquired channel ${ch.id().asShortText()}, number of active leases: $activeLeases"
}
}
override fun channelCreated(ch: Channel) {
val connectionId = connectionCount.incrementAndGet()
log.debug {
"Created connection ${ch.id().asShortText()}, total number of active connections: $connectionId"
}
ch.closeFuture().addListener {
val activeConnections = connectionCount.decrementAndGet()
log.debug {
"Closed connection ${
ch.id().asShortText()
}, total number of active connections: $activeConnections"
}
}
val pipeline: ChannelPipeline = ch.pipeline()
profile.connection?.also { conn ->
val readIdleTimeout = conn.readIdleTimeout.toMillis()
val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
val idleTimeout = conn.idleTimeout.toMillis()
if (readIdleTimeout > 0 || writeIdleTimeout > 0 || idleTimeout > 0) {
pipeline.addLast(
IdleStateHandler(
true,
readIdleTimeout,
writeIdleTimeout,
idleTimeout,
TimeUnit.MILLISECONDS
)
)
}
}
// Add SSL handler if needed
if ("https".equals(scheme, ignoreCase = true)) {
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc(), host, port))
}
// HTTP handlers
pipeline.addLast("codec", HttpClientCodec())
if (profile.compressionEnabled) {
pipeline.addLast("decompressor", HttpContentDecompressor())
}
pipeline.addLast("aggregator", HttpObjectAggregator(134217728))
pipeline.addLast("chunked", ChunkedWriteHandler())
}
}
pool = FixedChannelPool(bootstrap, channelPoolHandler, profile.maxConnections)
}
private fun executeWithRetry(operation: () -> CompletableFuture<FullHttpResponse>): CompletableFuture<FullHttpResponse> {
val retryPolicy = profile.retryPolicy
return if (retryPolicy != null) {
val outcomeHandler = OutcomeHandler<FullHttpResponse> { outcome ->
when (outcome) {
is OperationOutcome.Success -> {
val response = outcome.result
val status = response.status()
when (status) {
HttpResponseStatus.TOO_MANY_REQUESTS -> {
val retryAfter = response.headers()[HttpHeaderNames.RETRY_AFTER]?.let { headerValue ->
try {
headerValue.toLong() * 1000
} catch (nfe: NumberFormatException) {
null
}
}
OutcomeHandlerResult.Retry(retryAfter)
}
HttpResponseStatus.INTERNAL_SERVER_ERROR, HttpResponseStatus.SERVICE_UNAVAILABLE ->
OutcomeHandlerResult.Retry()
else -> OutcomeHandlerResult.DoNotRetry()
}
}
is OperationOutcome.Failure -> {
OutcomeHandlerResult.Retry()
}
}
}
executeWithRetry(
group,
retryPolicy.maxAttempts,
retryPolicy.initialDelayMillis.toDouble(),
retryPolicy.exp,
outcomeHandler,
Random.Default,
operation
)
} else {
operation()
}
}
fun healthCheck(nonce: ByteArray): CompletableFuture<ByteArray?> {
return executeWithRetry {
sendRequest(profile.serverURI, HttpMethod.TRACE, nonce)
}.thenApply {
val status = it.status()
if (it.status() != HttpResponseStatus.OK) {
throw HttpException(status)
} else {
it.content()
}
}.thenApply { maybeByteBuf ->
maybeByteBuf?.let {
val result = ByteArray(it.readableBytes())
it.getBytes(0, result)
result
}
}
}
fun get(key: String): CompletableFuture<ByteArray?> {
return executeWithRetry {
sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)
}.thenApply {
val status = it.status()
if (it.status() == HttpResponseStatus.NOT_FOUND) {
null
} else 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 put(key: String, content: ByteArray, metadata: CacheValueMetadata): CompletableFuture<Unit> {
return executeWithRetry {
val extraHeaders = sequenceOf(
metadata.mimeType?.let { HttpHeaderNames.CONTENT_TYPE to it },
metadata.contentDisposition?.let { HttpHeaderNames.CONTENT_DISPOSITION to it }
).filterNotNull()
sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content, extraHeaders.asIterable())
}.thenApply {
val status = it.status()
if (it.status() != HttpResponseStatus.CREATED && it.status() != HttpResponseStatus.OK) {
throw HttpException(status)
}
}
}
private fun sendRequest(
uri: URI,
method: HttpMethod,
body: ByteArray?,
extraHeaders: Iterable<Pair<CharSequence, CharSequence>>? = null
): CompletableFuture<FullHttpResponse> {
val responseFuture = CompletableFuture<FullHttpResponse>()
// Custom handler for processing responses
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
override fun operationComplete(channelFuture: Future<Channel>) {
if (channelFuture.isSuccess) {
val channel = channelFuture.now
val pipeline = channel.pipeline()
val closeListener = GenericFutureListener<Future<Void>> {
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
}
channel.closeFuture().addListener(closeListener)
val responseHandler = object : SimpleChannelInboundHandler<FullHttpResponse>() {
override fun handlerAdded(ctx: ChannelHandlerContext) {
channel.closeFuture().removeListener(closeListener)
}
override fun channelRead0(
ctx: ChannelHandlerContext,
response: FullHttpResponse
) {
pipeline.remove(this)
responseFuture.complete(response)
if(!profile.connection.requestPipelining) {
pool.release(channel)
}
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
ctx.newPromise()
val ex = when (cause) {
is DecoderException -> cause.cause
else -> cause
}
responseFuture.completeExceptionally(ex)
ctx.close()
}
override fun channelInactive(ctx: ChannelHandlerContext) {
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
if(!profile.connection.requestPipelining) {
pool.release(channel)
}
super.channelInactive(ctx)
}
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is IdleStateEvent) {
val te = when (evt.state()) {
IdleState.READER_IDLE -> TimeoutException(
"Read timeout",
)
IdleState.WRITER_IDLE -> TimeoutException("Write timeout")
IdleState.ALL_IDLE -> TimeoutException("Idle timeout")
null -> throw IllegalStateException("This should never happen")
}
responseFuture.completeExceptionally(te)
super.userEventTriggered(ctx, evt)
if (this === pipeline.last()) {
ctx.close()
}
if(!profile.connection.requestPipelining) {
pool.release(channel)
}
} else {
super.userEventTriggered(ctx, evt)
}
}
}
pipeline.addLast(responseHandler)
// Prepare the HTTP request
val request: FullHttpRequest = let {
val content: ByteBuf? = body?.takeIf(ByteArray::isNotEmpty)?.let(Unpooled::wrappedBuffer)
DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
method,
uri.rawPath,
content ?: Unpooled.buffer(0)
).apply {
// Set headers
headers().apply {
if (content != null) {
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
}
set(HttpHeaderNames.HOST, profile.serverURI.host)
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
if (profile.compressionEnabled) {
set(
HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
)
}
extraHeaders?.forEach { (k, v) ->
add(k, v)
}
// Add basic auth if configured
(profile.authentication as? Configuration.Authentication.BasicAuthenticationCredentials)?.let { credentials ->
val auth = "${credentials.username}:${credentials.password}"
val encodedAuth = Base64.getEncoder().encodeToString(auth.toByteArray())
set(HttpHeaderNames.AUTHORIZATION, "Basic $encodedAuth")
}
}
}
}
// Send the request
channel.writeAndFlush(request).addListener {
if(!it.isSuccess) {
val ex = it.cause()
log.warn(ex.message, ex)
}
if(profile.connection.requestPipelining) {
pool.release(channel)
}
}
} else {
responseFuture.completeExceptionally(channelFuture.cause())
}
}
})
return responseFuture
}
fun shutDown(): NettyFuture<*> {
return group.shutdownGracefully()
}
override fun close() {
shutDown().sync()
}
}

View File

@@ -0,0 +1,151 @@
package net.woggioni.rbcs.client.impl
import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.temporal.ChronoUnit
object Parser {
fun parse(document: Document): Configuration {
val root = document.documentElement
val profiles = mutableMapOf<String, Configuration.Profile>()
for (child in root.asIterable()) {
val tagName = child.localName
when (tagName) {
"profile" -> {
val name =
child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
val uri = child.renderAttribute("base-url")?.let(::URI)
?: throw ConfigurationException("base-url attribute is required")
var authentication: Configuration.Authentication? = null
var retryPolicy: Configuration.RetryPolicy? = null
var connection : Configuration.Connection = Configuration.Connection(
Duration.ofSeconds(60),
Duration.ofSeconds(60),
Duration.ofSeconds(30),
false
)
var trustStore : Configuration.TrustStore? = null
for (gchild in child.asIterable()) {
when (gchild.localName) {
"tls-client-auth" -> {
val keyStoreFile = gchild.renderAttribute("key-store-file")
val keyStorePassword =
gchild.renderAttribute("key-store-password")
val keyAlias = gchild.renderAttribute("key-alias")
val keyPassword = gchild.renderAttribute("key-password")
val keystore = KeyStore.getInstance("PKCS12").apply {
Files.newInputStream(Path.of(keyStoreFile)).use {
load(it, keyStorePassword?.toCharArray())
}
}
val key = keystore.getKey(keyAlias, keyPassword?.toCharArray()) as PrivateKey
val certChain = keystore.getCertificateChain(keyAlias).asSequence()
.map { it as X509Certificate }
.toList()
.toTypedArray()
authentication =
Configuration.Authentication.TlsClientAuthenticationCredentials(
key,
certChain
)
}
"basic-auth" -> {
val username = gchild.renderAttribute("user")
?: throw ConfigurationException("username attribute is required")
val password = gchild.renderAttribute("password")
?: throw ConfigurationException("password attribute is required")
authentication =
Configuration.Authentication.BasicAuthenticationCredentials(
username,
password
)
}
"retry-policy" -> {
val maxAttempts =
gchild.renderAttribute("max-attempts")
?.let(String::toInt)
?: throw ConfigurationException("max-attempts attribute is required")
val initialDelay =
gchild.renderAttribute("initial-delay")
?.let(Duration::parse)
?: Duration.ofSeconds(1)
val exp =
gchild.renderAttribute("exp")
?.let(String::toDouble)
?: 2.0f
retryPolicy = Configuration.RetryPolicy(
maxAttempts,
initialDelay.toMillis(),
exp.toDouble()
)
}
"connection" -> {
val idleTimeout = gchild.renderAttribute("idle-timeout")
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
val readIdleTimeout = gchild.renderAttribute("read-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val requestPipelining = gchild.renderAttribute("request-pipelining")
?.let(String::toBoolean) ?: false
connection = Configuration.Connection(
readIdleTimeout,
writeIdleTimeout,
idleTimeout,
requestPipelining
)
}
"tls-trust-store" -> {
val file = gchild.renderAttribute("file")
?.let(Path::of)
val password = gchild.renderAttribute("password")
val checkCertificateStatus = gchild.renderAttribute("check-certificate-status")
?.let(String::toBoolean) ?: false
val verifyServerCertificate = gchild.renderAttribute("verify-server-certificate")
?.let(String::toBoolean) ?: true
trustStore = Configuration.TrustStore(file, password, checkCertificateStatus, verifyServerCertificate)
}
}
}
val maxConnections = child.renderAttribute("max-connections")
?.let(String::toInt)
?: 50
val connectionTimeout = child.renderAttribute("connection-timeout")
?.let(Duration::parse)
val compressionEnabled = child.renderAttribute("enable-compression")
?.let(String::toBoolean)
?: true
profiles[name] = Configuration.Profile(
uri,
connection,
authentication,
connectionTimeout,
maxConnections,
compressionEnabled,
retryPolicy,
trustStore
)
}
}
}
return Configuration(profiles)
}
}

View File

@@ -0,0 +1,79 @@
package net.woggioni.rbcs.client
import io.netty.util.concurrent.EventExecutorGroup
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.math.pow
import kotlin.random.Random
sealed class OperationOutcome<T> {
class Success<T>(val result: T) : OperationOutcome<T>()
class Failure<T>(val ex: Throwable) : OperationOutcome<T>()
}
sealed class OutcomeHandlerResult {
class Retry(val suggestedDelayMillis: Long? = null) : OutcomeHandlerResult()
class DoNotRetry : OutcomeHandlerResult()
}
fun interface OutcomeHandler<T> {
fun shouldRetry(result: OperationOutcome<T>): OutcomeHandlerResult
}
fun <T> executeWithRetry(
eventExecutorGroup: EventExecutorGroup,
maxAttempts: Int,
initialDelay: Double,
exp: Double,
outcomeHandler: OutcomeHandler<T>,
randomizer : Random?,
cb: () -> CompletableFuture<T>
): CompletableFuture<T> {
val finalResult = cb()
var future = finalResult
var shortCircuit = false
for (i in 1 until maxAttempts) {
future = future.handle { result, ex ->
val operationOutcome = if (ex == null) {
OperationOutcome.Success(result)
} else {
OperationOutcome.Failure(ex.cause ?: ex)
}
if (shortCircuit) {
when(operationOutcome) {
is OperationOutcome.Failure -> throw operationOutcome.ex
is OperationOutcome.Success -> CompletableFuture.completedFuture(operationOutcome.result)
}
} else {
when(val outcomeHandlerResult = outcomeHandler.shouldRetry(operationOutcome)) {
is OutcomeHandlerResult.Retry -> {
val res = CompletableFuture<T>()
val delay = run {
val scheduledDelay = (initialDelay * exp.pow(i.toDouble()) * (1.0 + (randomizer?.nextDouble(-0.5, 0.5) ?: 0.0))).toLong()
outcomeHandlerResult.suggestedDelayMillis?.coerceAtMost(scheduledDelay) ?: scheduledDelay
}
eventExecutorGroup.schedule({
cb().handle { result, ex ->
if (ex == null) {
res.complete(result)
} else {
res.completeExceptionally(ex)
}
}
}, delay, TimeUnit.MILLISECONDS)
res
}
is OutcomeHandlerResult.DoNotRetry -> {
shortCircuit = true
when(operationOutcome) {
is OperationOutcome.Failure -> throw operationOutcome.ex
is OperationOutcome.Success -> CompletableFuture.completedFuture(operationOutcome.result)
}
}
}
}
}.thenCompose { it }
}
return future
}

View File

@@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.rbcs.client"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:rbcs-client="urn:net.woggioni.rbcs.client"
elementFormDefault="unqualified"
>
<xs:element name="profiles" type="rbcs-client:profilesType"/>
<xs:complexType name="profilesType">
<xs:sequence minOccurs="0">
<xs:element name="profile" type="rbcs-client:profileType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="profileType">
<xs:sequence>
<xs:choice>
<xs:element name="no-auth" type="rbcs-client:noAuthType">
<xs:annotation>
<xs:documentation>
Disable authentication.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="basic-auth" type="rbcs-client:basicAuthType">
<xs:annotation>
<xs:documentation>
Enable HTTP basic authentication.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType">
<xs:annotation>
<xs:documentation>
Enable TLS certificate authentication.
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
<xs:element name="connection" type="rbcs-client:connectionType" minOccurs="0" >
<xs:annotation>
<xs:documentation>
Set inactivity timeouts for connections to this server,
if not present, connections are only closed on network errors.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="retry-policy" type="rbcs-client:retryType" minOccurs="0">
<xs:annotation>
<xs:documentation>
Set a retry policy for this server, if not present requests won't be retried
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="tls-trust-store" type="rbcs-client:trustStoreType" minOccurs="0">
<xs:annotation>
<xs:documentation>
If set, specify an alternative truststore to validate the server certificate.
If not present the system truststore is used.
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>
Name of this server profile, to be referred to from rbcs-cli with the '-p' parameter
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="base-url" type="xs:anyURI" use="required">
<xs:annotation>
<xs:documentation>
RBCs server URL
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50">
<xs:annotation>
<xs:documentation>
Maximum number of concurrent TCP connection to open with this server
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="connection-timeout" type="xs:duration">
<xs:annotation>
<xs:documentation>
Enable HTTP compression when communicating to this server
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="enable-compression" type="xs:boolean" default="true">
<xs:annotation>
<xs:documentation>
Enable HTTP compression when communicating to this server
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="connectionType">
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S">
<xs:annotation>
<xs:documentation>
The client will close the connection with the server
when neither a read nor a write was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S">
<xs:annotation>
<xs:documentation>
The client will close the connection with the server
when no read was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S">
<xs:annotation>
<xs:documentation>
The client will close the connection with the server
when no write was performed for the specified period of time.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="request-pipelining" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>
Enables HTTP/1.1 request pipelining
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="noAuthType">
<xs:annotation>
<xs:documentation>
Add this tag to not use any type of authentication when talking to the RBCS server
</xs:documentation>
</xs:annotation>
</xs:complexType>
<xs:complexType name="basicAuthType">
<xs:annotation>
<xs:documentation>
Add this tag to enable HTTP basic authentication for the communication to this server,
mind that HTTP basic authentication sends credentials directly over the network, so make sure
your communication is protected by TLS (i.e. your server's URL starts with "https")
</xs:documentation>
</xs:annotation>
<xs:attribute name="user" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>
Username for HTTP basic authentication
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Password used for HTTP basic authentication
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="tlsClientAuthType">
<xs:attribute name="key-store-file" type="xs:anyURI" use="required">
<xs:annotation>
<xs:documentation>
System path to the keystore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-store-password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Password to open they keystore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-alias" type="xs:token" use="required">
<xs:annotation>
<xs:documentation>
Alias of the keystore entry containing the private key
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-password" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
Private key entry's encryption password
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="retryType">
<xs:annotation>
<xs:documentation>
Retry policy to use in case of failures, based on exponential backoff
https://en.wikipedia.org/wiki/Exponential_backoff
</xs:documentation>
</xs:annotation>
<xs:attribute name="max-attempts" type="xs:positiveInteger" use="required">
<xs:annotation>
<xs:documentation>
Maximum number of attempts, after which the call will result in an error,
throwing an exception related to the last received failure
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S">
<xs:annotation>
<xs:documentation>
Delay to apply before retrying after the first failed call
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="exp" type="xs:double" default="2.0">
<xs:annotation>
<xs:documentation>
Exponent to apply to compute the next delay
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="trustStoreType">
<xs:attribute name="file" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Path to the truststore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string">
<xs:annotation>
<xs:documentation>
Truststore file password
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="check-certificate-status" type="xs:boolean">
<xs:annotation>
<xs:documentation>
Whether or not check the server certificate validity using CRL/OCSP
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="verify-server-certificate" type="xs:boolean" use="optional" default="true">
<xs:annotation>
<xs:documentation>
If false, the client will blindly trust the certificate provided by the server
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:schema>

View File

@@ -0,0 +1,148 @@
package net.woggioni.rbcs.client
import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.rbcs.common.contextLogger
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider
import org.junit.jupiter.params.provider.ArgumentsSource
import java.util.concurrent.CompletableFuture
import java.util.stream.Stream
import kotlin.random.Random
class RetryTest {
data class TestArgs(
val seed: Int,
val maxAttempt: Int,
val initialDelay: Double,
val exp: Double,
)
class TestArguments : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext): Stream<out Arguments> {
return Stream.of(
TestArgs(
seed = 101325,
maxAttempt = 5,
initialDelay = 50.0,
exp = 2.0,
),
TestArgs(
seed = 101325,
maxAttempt = 20,
initialDelay = 100.0,
exp = 1.1,
),
TestArgs(
seed = 123487,
maxAttempt = 20,
initialDelay = 100.0,
exp = 2.0,
),
TestArgs(
seed = 20082024,
maxAttempt = 10,
initialDelay = 100.0,
exp = 2.0,
)
).map {
object: Arguments {
override fun get() = arrayOf(it)
}
}
}
}
@ArgumentsSource(TestArguments::class)
@ParameterizedTest
fun test(testArgs: TestArgs) {
val log = contextLogger()
log.debug("Start")
val executor: EventExecutorGroup = DefaultEventExecutorGroup(1)
val attempts = mutableListOf<Pair<Long, OperationOutcome<Int>>>()
val outcomeHandler = OutcomeHandler<Int> { outcome ->
when(outcome) {
is OperationOutcome.Success -> {
if(outcome.result % 10 == 0) {
OutcomeHandlerResult.DoNotRetry()
} else {
OutcomeHandlerResult.Retry(null)
}
}
is OperationOutcome.Failure -> {
when(outcome.ex) {
is IllegalStateException -> {
log.debug(outcome.ex.message, outcome.ex)
OutcomeHandlerResult.Retry(null)
}
else -> {
OutcomeHandlerResult.DoNotRetry()
}
}
}
}
}
val random = Random(testArgs.seed)
val future =
executeWithRetry(executor, testArgs.maxAttempt, testArgs.initialDelay, testArgs.exp, outcomeHandler, null) {
val now = System.nanoTime()
val result = CompletableFuture<Int>()
executor.submit {
val n = random.nextInt(0, Integer.MAX_VALUE)
log.debug("Got new number: {}", n)
if(n % 3 == 0) {
val ex = IllegalStateException("Value $n can be divided by 3")
result.completeExceptionally(ex)
attempts += now to OperationOutcome.Failure(ex)
} else if(n % 7 == 0) {
val ex = RuntimeException("Value $n can be divided by 7")
result.completeExceptionally(ex)
attempts += now to OperationOutcome.Failure(ex)
} else {
result.complete(n)
attempts += now to OperationOutcome.Success(n)
}
}
result
}
Assertions.assertTrue(attempts.size <= testArgs.maxAttempt)
val result = future.handle { res, ex ->
if(ex != null) {
val err = ex.cause ?: ex
log.debug(err.message, err)
OperationOutcome.Failure(err)
} else {
OperationOutcome.Success(res)
}
}.get()
for ((index, attempt) in attempts.withIndex()) {
val (timestamp, value) = attempt
if (index > 0) {
/* Check the delay for subsequent attempts is correct */
val previousAttempt = attempts[index - 1]
val expectedTimestamp =
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
val actualTimestamp = timestamp
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
Assertions.assertTrue(err < 0.1)
}
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
/*
* If the last attempt index is lower than the maximum number of attempts, then
* check the outcome handler returns DoNotRetry
*/
Assertions.assertTrue(outcomeHandler.shouldRetry(value) is OutcomeHandlerResult.DoNotRetry)
} else if (index < attempts.size - 1) {
/*
* If the attempt is not the last attempt check the outcome handler returns Retry
*/
Assertions.assertTrue(outcomeHandler.shouldRetry(value) is OutcomeHandlerResult.Retry)
}
}
}
}

View File

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

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rbcs-client="urn:net.woggioni.rbcs.client"
xs:schemaLocation="urn:net.woggioni.rbcs.client jms://net.woggioni.rbcs.client/net/woggioni/rbcs/client/schema/rbcs-client.xsd"
>
<profile name="profile1" base-url="https://rbcs1.example.com/">
<tls-client-auth
key-store-file="keystore.pfx"
key-store-password="password"
key-alias="woggioni@c962475fa38"
key-password="key-password"/>
<connection write-idle-timeout="PT60S" read-idle-timeout="PT60S" write-timeout="PT0S" read-timeout="PT0S" idle-timeout="PT30S" />
<tls-trust-store file="file.pfx" password="password" check-certificate-status="false" verify-server-certificate="true"/>
</profile>
<profile name="profile2" base-url="https://rbcs2.example.com/">
<basic-auth user="user" password="password"/>
</profile>
</rbcs-client:profiles>

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