Compare commits
43 Commits
0.2.0-alph
...
52a1b4c200
Author | SHA1 | Date | |
---|---|---|---|
52a1b4c200
|
|||
559ad5e528
|
|||
fd0bd1ee5f
|
|||
0e92998f16
|
|||
9eef91ebba
|
|||
3416c327b9
|
|||
9bdaa0d32e
|
|||
206bcd6319
|
|||
3774ab8ef0
|
|||
303828392e
|
|||
5d8cbe34ef
|
|||
85c0d4a384
|
|||
ae8817ad2a
|
|||
69f215e68f
|
|||
222b475223
|
|||
ede515e2ca
|
|||
974fdb7a91
|
|||
a294229ff0
|
|||
9600dd7e4f
|
|||
729276a2b1
|
|||
7ba7070693
|
|||
59a12d6218
|
|||
fc298de548
|
|||
8b639fc0b3
|
|||
5545f618f9
|
|||
43c0938d9a
|
|||
17215b401a
|
|||
4aced1c717
|
|||
31ce34cddb
|
|||
d64f7f4f27
|
|||
d15235fc4c
|
|||
49bb4f41b8
|
|||
a1398045ac
|
|||
1f93602102
|
|||
c818463a2e
|
|||
cd28563985
|
|||
8ef2d9c64e
|
|||
1510956989
|
|||
ac4f0fdd19
|
|||
37da03c719
|
|||
60bc4375cf
|
|||
725fe22b80
|
|||
ca18b63f27
|
80
.gitea/workflows/build-dev.yaml
Normal file
80
.gitea/workflows/build-dev.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: woryzen
|
||||
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
|
||||
|
@@ -5,7 +5,7 @@ on:
|
||||
- '*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: hostinger
|
||||
runs-on: woryzen
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
@@ -39,9 +39,9 @@ jobs:
|
||||
push: true
|
||||
pull: true
|
||||
tags: |
|
||||
gitea.woggioni.net/woggioni/rbcs:latest
|
||||
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release
|
||||
gitea.woggioni.net/woggioni/rbcs:vanilla
|
||||
gitea.woggioni.net/woggioni/rbcs:vanilla-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release-vanilla
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
-
|
||||
name: Build rbcs memcache Docker image
|
||||
@@ -52,11 +52,37 @@ jobs:
|
||||
push: true
|
||||
pull: true
|
||||
tags: |
|
||||
gitea.woggioni.net/woggioni/rbcs:latest
|
||||
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
|
||||
gitea.woggioni.net/woggioni/rbcs:memcache
|
||||
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
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
|
||||
env:
|
||||
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
|
||||
|
274
README.md
274
README.md
@@ -1,4 +1,45 @@
|
||||
# Remote Build Cache Server
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<!--
|
||||

|
||||
-->
|
||||
|
||||
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
|
||||
@@ -12,9 +53,10 @@ and throttling.
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Downloading the jar file
|
||||
### Use the all-in-one jar file
|
||||
You can download the latest version from [this link](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/)
|
||||
|
||||
|
||||
Assuming you have Java 21 or later installed, you can launch the server directly with
|
||||
|
||||
```bash
|
||||
@@ -24,7 +66,7 @@ java -jar rbcs-cli.jar server
|
||||
By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication,
|
||||
writing data to the disk, that you can use for testing
|
||||
|
||||
### Using the Docker image
|
||||
### Use the Docker image
|
||||
You can pull the latest Docker image with
|
||||
```bash
|
||||
docker pull gitea.woggioni.net/woggioni/rbcs:latest
|
||||
@@ -33,30 +75,20 @@ 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
|
||||
|
||||
## Usage
|
||||
### 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.
|
||||
|
||||
### Configuration
|
||||
The location of the `rbcs.xml` configuration file depends on the operating system,
|
||||
Alternatively it can be changed setting the `RBCS_CONFIGURATION_DIR` environmental variable or `net.woggioni.rbcs.conf.dir` Java system property
|
||||
to the directory that contain the `rbcs.xml` file.
|
||||
> [!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)
|
||||
|
||||
The server configuration file follows the XML format and uses XML schema for validation
|
||||
(you can find the schema for the main configuration file [here](https://gitea.woggioni.net/woggioni/rbcs/src/branch/master/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs.xsd)).
|
||||
## Integration with build tools
|
||||
|
||||
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.
|
||||
|
||||
Full documentation for all tags and attributes is available [here](doc/server_configuration.md).
|
||||
|
||||
### Plugins
|
||||
If you want to use memcache as a storage backend you'll also need to download [the memcache plugin](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-server-memcache/)
|
||||
|
||||
Plugins need to be stored in a folder named `plugins` in the located server's working directory
|
||||
(the directory where the server process is started). They are shipped as TAR archives, so you need to extract
|
||||
the content of the archive into the `plugins` directory for the server to pick them up.
|
||||
|
||||
### Using RBCS with Gradle
|
||||
### Use RBCS with Gradle
|
||||
|
||||
Add this to the `settings.gradle` file of your project
|
||||
|
||||
@@ -101,7 +133,7 @@ add `org.gradle.caching=true` to your `<project>/gradle.properties` or run gradl
|
||||
|
||||
Read [Gradle documentation](https://docs.gradle.org/current/userguide/build_cache.html) for more detailed information.
|
||||
|
||||
### Using RBCS with Maven
|
||||
### Use RBCS with Maven
|
||||
|
||||
1. Create an `extensions.xml` in `<project>/.mvn/extensions.xml` with the following content
|
||||
```xml
|
||||
@@ -128,10 +160,202 @@ Read [Gradle documentation](https://docs.gradle.org/current/userguide/build_cach
|
||||
|
||||
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?
|
||||
|
||||
|
94
benchmark/rbcs-filesystem.yml
Normal file
94
benchmark/rbcs-filesystem.yml
Normal 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
|
||||
|
77
benchmark/rbcs-in-memory.yml
Normal file
77
benchmark/rbcs-in-memory.yml
Normal 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
118
benchmark/rbcs-memcache.yml
Normal 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
|
@@ -39,7 +39,6 @@ allprojects { subproject ->
|
||||
modularity.inferModulePath = true
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
vendor = JvmVendorSpec.ORACLE
|
||||
}
|
||||
}
|
||||
|
||||
|
86
doc/benchmarks.md
Normal file
86
doc/benchmarks.md
Normal 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
125
doc/client_configuration.md
Normal 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
189
doc/server_configuration.md
Normal 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>
|
||||
|
||||
```
|
@@ -3,9 +3,9 @@ RUN adduser -D luser
|
||||
USER luser
|
||||
WORKDIR /home/luser
|
||||
|
||||
FROM base-release AS release
|
||||
FROM base-release AS release-vanilla
|
||||
ADD rbcs-cli-envelope-*.jar rbcs.jar
|
||||
ENTRYPOINT ["java", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.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-memcache
|
||||
ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.jar
|
||||
@@ -14,4 +14,29 @@ WORKDIR /home/luser/plugins
|
||||
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar
|
||||
WORKDIR /home/luser
|
||||
ADD logback.xml .
|
||||
ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"]
|
||||
ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=70", "-XX:GCTimeRatio=24", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar"]
|
||||
|
||||
FROM busybox:musl AS base-native
|
||||
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
28
docker/README.md
Normal 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.
|
@@ -29,7 +29,7 @@ Provider<Copy> prepareDockerBuild = tasks.register('prepareDockerBuild', Copy) {
|
||||
group = 'docker'
|
||||
into project.layout.buildDirectory.file('docker')
|
||||
from(configurations.docker)
|
||||
from(file('Dockerfile'))
|
||||
from(files('Dockerfile', 'rbcs-cli.sh'))
|
||||
from(rootProject.file('conf')) {
|
||||
include 'logback.xml'
|
||||
}
|
||||
|
3
docker/rbcs-cli.sh
Normal file
3
docker/rbcs-cli.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
DIR=/usr/share/java/rbcs
|
||||
$DIR/bin/java $JAVA_OPTS -m net.woggioni.rbcs.cli "$@"
|
@@ -2,9 +2,9 @@ org.gradle.configuration-cache=false
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
rbcs.version = 0.2.0
|
||||
rbcs.version = 0.3.1
|
||||
|
||||
lys.version = 2025.02.08
|
||||
lys.version = 2025.06.10
|
||||
|
||||
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
||||
docker.registry.url=gitea.woggioni.net
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
3
gradlew
vendored
3
gradlew
vendored
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# 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
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@@ -5,9 +5,12 @@ plugins {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@@ -1,10 +1,15 @@
|
||||
module net.woggioni.rbcs.api {
|
||||
requires static lombok;
|
||||
requires java.xml;
|
||||
requires io.netty.buffer;
|
||||
requires io.netty.handler;
|
||||
requires io.netty.transport;
|
||||
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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
package net.woggioni.rbcs.api;
|
||||
|
||||
import io.netty.channel.ChannelFactory;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
|
||||
public interface CacheHandlerFactory extends AsyncCloseable {
|
||||
ChannelHandler newHandler(
|
||||
CacheHandler newHandler(
|
||||
Configuration configuration,
|
||||
EventLoopGroup eventLoopGroup,
|
||||
ChannelFactory<SocketChannel> socketChannelFactory,
|
||||
ChannelFactory<DatagramChannel> datagramChannelFactory
|
||||
|
@@ -21,6 +21,8 @@ public class Configuration {
|
||||
@NonNull
|
||||
EventExecutor eventExecutor;
|
||||
@NonNull
|
||||
RateLimiter rateLimiter;
|
||||
@NonNull
|
||||
Connection connection;
|
||||
Map<String, User> users;
|
||||
Map<String, Group> groups;
|
||||
@@ -28,6 +30,13 @@ public class Configuration {
|
||||
Authentication authentication;
|
||||
Tls tls;
|
||||
|
||||
@Value
|
||||
public static class RateLimiter {
|
||||
boolean delayRequest;
|
||||
int messageBufferSize;
|
||||
int maxQueuedMessages;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class EventExecutor {
|
||||
boolean useVirtualThreads;
|
||||
@@ -39,6 +48,7 @@ public class Configuration {
|
||||
Duration readIdleTimeout;
|
||||
Duration writeIdleTimeout;
|
||||
int maxRequestSize;
|
||||
int chunkSize;
|
||||
}
|
||||
|
||||
@Value
|
||||
@@ -133,6 +143,7 @@ public class Configuration {
|
||||
int incomingConnectionsBacklogSize,
|
||||
String serverPath,
|
||||
EventExecutor eventExecutor,
|
||||
RateLimiter rateLimiter,
|
||||
Connection connection,
|
||||
Map<String, User> users,
|
||||
Map<String, Group> groups,
|
||||
@@ -146,6 +157,7 @@ public class Configuration {
|
||||
incomingConnectionsBacklogSize,
|
||||
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
|
||||
eventExecutor,
|
||||
rateLimiter,
|
||||
connection,
|
||||
users,
|
||||
groups,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package net.woggioni.rbcs.api;
|
||||
|
||||
public enum Role {
|
||||
Reader, Writer
|
||||
Reader, Writer, Healthcheck
|
||||
}
|
@@ -14,17 +14,26 @@ public sealed interface CacheMessage {
|
||||
private final String key;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
abstract sealed class CacheGetResponse implements CacheMessage {
|
||||
private final String key;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
final class CacheValueFoundResponse extends CacheGetResponse {
|
||||
private final String key;
|
||||
private final CacheValueMetadata metadata;
|
||||
|
||||
public CacheValueFoundResponse(String key, CacheValueMetadata metadata) {
|
||||
super(key);
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
final class CacheValueNotFoundResponse extends CacheGetResponse {
|
||||
public CacheValueNotFoundResponse(String key) {
|
||||
super(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
@@ -9,24 +9,48 @@ plugins {
|
||||
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.NativeImagePlugin
|
||||
import net.woggioni.gradle.graalvm.NativeImageTask
|
||||
import net.woggioni.gradle.graalvm.JlinkPlugin
|
||||
import net.woggioni.gradle.graalvm.JlinkTask
|
||||
import net.woggioni.gradle.envelope.EnvelopePlugin
|
||||
import net.woggioni.gradle.graalvm.*
|
||||
|
||||
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
|
||||
@@ -37,6 +61,8 @@ dependencies {
|
||||
// runtimeOnly catalog.slf4j.jdk14
|
||||
runtimeOnly catalog.logback.classic
|
||||
// runtimeOnly catalog.slf4j.simple
|
||||
nativeImage project(':rbcs-server-memcache')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +75,8 @@ 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
|
||||
@@ -60,20 +88,65 @@ Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.E
|
||||
}
|
||||
|
||||
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
|
||||
mainClass = mainClassName
|
||||
mainModule = mainModuleName
|
||||
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())
|
||||
}
|
||||
|
||||
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
|
||||
nativeImage {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(23)
|
||||
vendor = JvmVendorSpec.GRAAL_VM
|
||||
}
|
||||
mainClass = mainClassName
|
||||
mainModule = mainModuleName
|
||||
// mainModule = mainModuleName
|
||||
useMusl = true
|
||||
buildStaticImage = true
|
||||
linkAtBuildTime = false
|
||||
classpath = project.files(jarTaskProvider, configurations.nativeImage)
|
||||
compressExecutable = true
|
||||
compressionLevel = 6
|
||||
useLZMA = false
|
||||
}
|
||||
|
||||
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
|
||||
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) {
|
||||
@@ -86,14 +159,21 @@ tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) {
|
||||
|
||||
artifacts {
|
||||
release(envelopeJarTaskProvider)
|
||||
release(upxTaskProvider)
|
||||
release(jlinkDistTarTaskProvider)
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
artifact envelopeJar
|
||||
artifact(upxTaskProvider) {
|
||||
classifier = "linux-x86_64"
|
||||
extension = "exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
15
rbcs-cli/conf/rbcs-client.xml
Normal file
15
rbcs-cli/conf/rbcs-client.xml
Normal 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>
|
53
rbcs-cli/conf/rbcs-server.xml
Normal file
53
rbcs-cli/conf/rbcs-server.xml
Normal 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>
|
6
rbcs-cli/native-image/jni-config.json
Normal file
6
rbcs-cli/native-image/jni-config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name":"java.lang.Boolean",
|
||||
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
|
||||
}
|
||||
]
|
@@ -1,2 +1,2 @@
|
||||
Args=-H:Optimize=3 --gc=serial --initialize-at-run-time=io.netty
|
||||
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
|
8
rbcs-cli/native-image/predefined-classes-config.json
Normal file
8
rbcs-cli/native-image/predefined-classes-config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"type":"agent-extracted",
|
||||
"classes":[
|
||||
]
|
||||
}
|
||||
]
|
||||
|
2
rbcs-cli/native-image/proxy-config.json
Normal file
2
rbcs-cli/native-image/proxy-config.json
Normal file
@@ -0,0 +1,2 @@
|
||||
[
|
||||
]
|
728
rbcs-cli/native-image/reflect-config.json
Normal file
728
rbcs-cli/native-image/reflect-config.json
Normal 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.jcraft.jzlib.JZlib"
|
||||
},
|
||||
{
|
||||
"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.MaxRequestSizeHandler",
|
||||
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
|
||||
},
|
||||
{
|
||||
"name":"net.woggioni.rbcs.server.handler.ReadTriggerDuplexHandler",
|
||||
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
|
||||
},
|
||||
{
|
||||
"name":"net.woggioni.rbcs.server.handler.ServerHandler",
|
||||
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"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.JavaKeyStore$JKS",
|
||||
"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.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"] }]
|
||||
}
|
||||
]
|
44
rbcs-cli/native-image/resource-config.json
Normal file
44
rbcs-cli/native-image/resource-config.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"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/schema/rbcs-server.xsd\\E"
|
||||
}]},
|
||||
"bundles":[{
|
||||
"name":"com.sun.org.apache.xerces.internal.impl.xpath.regex.message",
|
||||
"locales":[""]
|
||||
}]
|
||||
}
|
14
rbcs-cli/native-image/serialization-config.json
Normal file
14
rbcs-cli/native-image/serialization-config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"types":[
|
||||
{
|
||||
"name":"java.lang.String"
|
||||
},
|
||||
{
|
||||
"name":"net.woggioni.rbcs.api.CacheValueMetadata"
|
||||
}
|
||||
],
|
||||
"lambdaCapturingTypes":[
|
||||
],
|
||||
"proxies":[
|
||||
]
|
||||
}
|
@@ -0,0 +1,175 @@
|
||||
package net.woggioni.rbcs.cli.graal
|
||||
|
||||
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.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.client.Configuration as ClientConfiguration
|
||||
import net.woggioni.rbcs.client.impl.Parser as ClientConfigurationParser
|
||||
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
|
||||
|
||||
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),
|
||||
"someCustomPrefix",
|
||||
"MD5",
|
||||
null,
|
||||
1,
|
||||
)
|
||||
)
|
||||
|
||||
for (cache in caches) {
|
||||
val serverConfiguration = Configuration(
|
||||
"127.0.0.1",
|
||||
serverPort,
|
||||
100,
|
||||
null,
|
||||
Configuration.EventExecutor(true),
|
||||
Configuration.RateLimiter(
|
||||
false, 0x100000, 10
|
||||
),
|
||||
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,
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
@@ -11,7 +11,7 @@ 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.contextLogger
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Model.CommandSpec
|
||||
|
||||
@@ -23,15 +23,20 @@ class RemoteBuildCacheServerCli : RbcsCommand() {
|
||||
|
||||
class VersionProvider : AbstractVersionProvider()
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(vararg args: String) {
|
||||
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 = contextLogger()
|
||||
val log = createLogger<RemoteBuildCacheServerCli>()
|
||||
val app = Application.builder("rbcs")
|
||||
.configurationDirectoryEnvVar("RBCS_CONFIGURATION_DIR")
|
||||
.configurationDirectoryPropertyKey("net.woggioni.rbcs.conf.dir")
|
||||
@@ -51,7 +56,12 @@ class RemoteBuildCacheServerCli : RbcsCommand() {
|
||||
addSubcommand(GetCommand())
|
||||
addSubcommand(HealthCheckCommand())
|
||||
})
|
||||
System.exit(commandLine.execute(*args))
|
||||
return commandLine
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun main(vararg args: String) {
|
||||
System.exit(createCommandLine().execute(*args))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
package net.woggioni.rbcs.cli.impl
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.Manifest
|
||||
import picocli.CommandLine
|
||||
|
||||
|
||||
abstract class AbstractVersionProvider : CommandLine.IVersionProvider {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package net.woggioni.rbcs.cli.impl
|
||||
|
||||
import java.nio.file.Path
|
||||
import net.woggioni.jwo.Application
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
abstract class RbcsCommand : Runnable {
|
||||
@@ -12,7 +12,7 @@ abstract class RbcsCommand : Runnable {
|
||||
private set
|
||||
|
||||
protected fun findConfigurationFile(app: Application, fileName : String): Path {
|
||||
val confDir = app.computeConfigurationDirectory()
|
||||
val confDir = app.computeConfigurationDirectory(false)
|
||||
val configurationFile = confDir.resolve(fileName)
|
||||
return configurationFile
|
||||
}
|
||||
|
@@ -1,16 +1,5 @@
|
||||
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.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
|
||||
@@ -19,6 +8,18 @@ import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.random.Random
|
||||
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
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "benchmark",
|
||||
@@ -26,8 +27,136 @@ import kotlin.random.Random
|
||||
showDefaultValues = true
|
||||
)
|
||||
class BenchmarkCommand : RbcsCommand() {
|
||||
companion object{
|
||||
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 errorCounter = AtomicLong(0)
|
||||
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).handle { response, ex ->
|
||||
if(ex != null) {
|
||||
errorCounter.incrementAndGet()
|
||||
log.error(ex.message, ex)
|
||||
} else if (response == null) {
|
||||
errorCounter.incrementAndGet()
|
||||
log.error {
|
||||
"Missing entry for key '${entry.first}'"
|
||||
}
|
||||
} else if (!entry.second.contentEquals(response)) {
|
||||
errorCounter.incrementAndGet()
|
||||
log.error {
|
||||
"Retrieved a value different from what was inserted for key '${entry.first}': " +
|
||||
"expected '${JWO.bytesToHex(entry.second)}', got '${JWO.bytesToHex(response)}' instead"
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
val errors = errorCounter.get()
|
||||
val successfulRetrievals = entries.size - errors
|
||||
val successRate = successfulRetrievals.toDouble() / entries.size
|
||||
log.info {
|
||||
"Successfully retrieved ${entries.size - errors}/${entries.size} (${String.format("%.1f", successRate * 100)}%)"
|
||||
}
|
||||
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
|
||||
@@ -60,113 +189,11 @@ class BenchmarkCommand : RbcsCommand() {
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
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(randomValues) {
|
||||
random.nextBytes(size)
|
||||
} else {
|
||||
val byteValue = random.nextInt().toByte()
|
||||
ByteArray(size) {_ -> 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")
|
||||
}
|
||||
}
|
||||
execute(
|
||||
profile,
|
||||
numberOfEntries,
|
||||
size,
|
||||
randomValues
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,10 +1,12 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import java.nio.file.Path
|
||||
import net.woggioni.jwo.Application
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||
import net.woggioni.rbcs.client.Configuration
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Path
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "client",
|
||||
@@ -24,15 +26,20 @@ class ClientCommand(app : Application) : RbcsCommand() {
|
||||
names = ["-p", "--profile"],
|
||||
description = ["Name of the client profile to be used"],
|
||||
paramLabel = "PROFILE",
|
||||
required = true
|
||||
required = false
|
||||
)
|
||||
var profileName : String? = null
|
||||
get() = field ?: throw IllegalArgumentException("A profile name must be specified using the '-p' command line parameter")
|
||||
|
||||
val configuration : RemoteBuildCacheClient.Configuration by lazy {
|
||||
RemoteBuildCacheClient.Configuration.parse(configurationFile)
|
||||
val configuration : Configuration by lazy {
|
||||
Configuration.parse(configurationFile)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
val log = createLogger<ClientCommand>()
|
||||
log.debug {
|
||||
"Using configuration file '$configurationFile'"
|
||||
}
|
||||
println("Available profiles:")
|
||||
configuration.profiles.forEach { (profileName, _) ->
|
||||
println(profileName)
|
||||
|
@@ -1,11 +1,13 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
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.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "get",
|
||||
@@ -13,8 +15,20 @@ import java.nio.file.Path
|
||||
showDefaultValues = true
|
||||
)
|
||||
class GetCommand : RbcsCommand() {
|
||||
companion object{
|
||||
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
|
||||
@@ -40,14 +54,6 @@ class GetCommand : RbcsCommand() {
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
RemoteBuildCacheClient(profile).use { client ->
|
||||
client.get(key).thenApply { value ->
|
||||
value?.let {
|
||||
(output?.let(Files::newOutputStream) ?: System.out).use {
|
||||
it.write(value)
|
||||
}
|
||||
} ?: throw NoSuchElementException("No value found for key $key")
|
||||
}.get()
|
||||
}
|
||||
execute(profile, key, (output?.let(Files::newOutputStream) ?: System.out))
|
||||
}
|
||||
}
|
@@ -1,11 +1,12 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import java.security.SecureRandom
|
||||
import kotlin.random.Random
|
||||
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",
|
||||
@@ -15,6 +16,27 @@ import kotlin.random.Random
|
||||
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
|
||||
@@ -26,23 +48,6 @@ class HealthCheckCommand : RbcsCommand() {
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
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()
|
||||
}
|
||||
execute(profile)
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.PrintWriter
|
||||
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 java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.PrintWriter
|
||||
|
||||
|
||||
@CommandLine.Command(
|
||||
|
@@ -1,17 +1,18 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
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",
|
||||
@@ -19,8 +20,22 @@ import java.util.UUID
|
||||
showDefaultValues = true
|
||||
)
|
||||
class PutCommand : RbcsCommand() {
|
||||
companion object{
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,10 +107,7 @@ class PutCommand : RbcsCommand() {
|
||||
}
|
||||
actualKey = key ?: UUID.randomUUID().toString()
|
||||
}
|
||||
inputStream.use {
|
||||
client.put(actualKey, it.readAllBytes(), CacheValueMetadata(contentDisposition, mimeType))
|
||||
}.get()
|
||||
println(profile.serverURI.resolve(actualKey))
|
||||
execute(profile, actualKey, inputStream, mimeType, contentDisposition)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,10 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import net.woggioni.jwo.Application
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
@@ -10,11 +15,6 @@ 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",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package net.woggioni.rbcs.cli.impl.converters
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.time.Duration
|
||||
import picocli.CommandLine
|
||||
|
||||
|
||||
class DurationConverter : CommandLine.ITypeConverter<Duration> {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
package net.woggioni.rbcs.cli.impl.converters
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import picocli.CommandLine
|
||||
|
||||
|
||||
class InputStreamConverter : CommandLine.ITypeConverter<InputStream> {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
package net.woggioni.rbcs.cli.impl.converters
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import picocli.CommandLine
|
||||
|
||||
|
||||
class OutputStreamConverter : CommandLine.ITypeConverter<OutputStream> {
|
||||
|
@@ -0,0 +1,62 @@
|
||||
package net.woggioni.rbcs.client
|
||||
|
||||
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
|
||||
import net.woggioni.rbcs.client.impl.Parser
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,13 +4,13 @@ import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.Channel
|
||||
import io.netty.channel.ChannelHandler
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
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.NioEventLoopGroup
|
||||
import io.netty.channel.nio.NioIoHandler
|
||||
import io.netty.channel.pool.AbstractChannelPoolHandler
|
||||
import io.netty.channel.pool.ChannelPool
|
||||
import io.netty.channel.pool.FixedChannelPool
|
||||
@@ -34,91 +34,66 @@ 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 net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.client.impl.Parser
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.common.trace
|
||||
import java.io.IOException
|
||||
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.time.Duration
|
||||
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 io.netty.util.concurrent.Future as NettyFuture
|
||||
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{
|
||||
companion object {
|
||||
private val log = createLogger<RemoteBuildCacheClient>()
|
||||
}
|
||||
|
||||
private val group: NioEventLoopGroup
|
||||
private var sslContext: SslContext
|
||||
private val group: IoEventLoopGroup
|
||||
private val sslContext: SslContext
|
||||
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()
|
||||
}
|
||||
|
||||
class RetryPolicy(
|
||||
val maxAttempts: Int,
|
||||
val initialDelayMillis: Long,
|
||||
val exp: Double
|
||||
)
|
||||
|
||||
class Connection(
|
||||
val readTimeout: Duration,
|
||||
val writeTimeout: Duration,
|
||||
val idleTimeout: Duration,
|
||||
val readIdleTimeout: Duration,
|
||||
val writeIdleTimeout: Duration
|
||||
)
|
||||
|
||||
data class Profile(
|
||||
val serverURI: URI,
|
||||
val connection: Connection?,
|
||||
val authentication: Authentication?,
|
||||
val connectionTimeout: Duration?,
|
||||
val maxConnections: Int,
|
||||
val compressionEnabled: Boolean,
|
||||
val retryPolicy: RetryPolicy?,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun parse(path: Path): Configuration {
|
||||
return Files.newInputStream(path).use {
|
||||
Xml.parseXml(path.toUri().toURL(), it)
|
||||
}.let(Parser::parse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
group = NioEventLoopGroup()
|
||||
group = MultiThreadIoEventLoopGroup(NioIoHandler.newFactory())
|
||||
sslContext = SslContextBuilder.forClient().also { builder ->
|
||||
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
|
||||
builder.keyManager(
|
||||
tlsClientAuthenticationCredentials.key,
|
||||
*tlsClientAuthenticationCredentials.certificateChain
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -177,20 +152,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
||||
}
|
||||
val pipeline: ChannelPipeline = ch.pipeline()
|
||||
|
||||
profile.connection?.also { conn ->
|
||||
val readTimeout = conn.readTimeout.toMillis()
|
||||
val writeTimeout = conn.writeTimeout.toMillis()
|
||||
if (readTimeout > 0 || writeTimeout > 0) {
|
||||
pipeline.addLast(
|
||||
IdleStateHandler(
|
||||
false,
|
||||
readTimeout,
|
||||
writeTimeout,
|
||||
0,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
)
|
||||
}
|
||||
profile.connection.also { conn ->
|
||||
val readIdleTimeout = conn.readIdleTimeout.toMillis()
|
||||
val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
|
||||
val idleTimeout = conn.idleTimeout.toMillis()
|
||||
@@ -214,7 +176,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
||||
|
||||
// HTTP handlers
|
||||
pipeline.addLast("codec", HttpClientCodec())
|
||||
if(profile.compressionEnabled) {
|
||||
if (profile.compressionEnabled) {
|
||||
pipeline.addLast("decompressor", HttpContentDecompressor())
|
||||
}
|
||||
pipeline.addLast("aggregator", HttpObjectAggregator(134217728))
|
||||
@@ -333,50 +295,33 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
||||
): CompletableFuture<FullHttpResponse> {
|
||||
val responseFuture = CompletableFuture<FullHttpResponse>()
|
||||
// Custom handler for processing responses
|
||||
|
||||
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
||||
private val handlers = mutableListOf<ChannelHandler>()
|
||||
|
||||
fun cleanup(channel: Channel, pipeline: ChannelPipeline) {
|
||||
handlers.forEach(pipeline::remove)
|
||||
pool.release(channel)
|
||||
}
|
||||
|
||||
override fun operationComplete(channelFuture: Future<Channel>) {
|
||||
if (channelFuture.isSuccess) {
|
||||
val channel = channelFuture.now
|
||||
val pipeline = channel.pipeline()
|
||||
val timeoutHandler = object : ChannelInboundHandlerAdapter() {
|
||||
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)
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
val closeListener = GenericFutureListener<Future<Void>> {
|
||||
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
||||
pool.release(channel)
|
||||
}
|
||||
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
|
||||
) {
|
||||
channel.closeFuture().removeListener(closeListener)
|
||||
cleanup(channel, pipeline)
|
||||
pipeline.remove(this)
|
||||
responseFuture.complete(response)
|
||||
if (!profile.connection.requestPipelining) {
|
||||
pool.release(channel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
@@ -390,16 +335,33 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
||||
}
|
||||
|
||||
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||
pool.release(channel)
|
||||
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
||||
super.channelInactive(ctx)
|
||||
pool.release(channel)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (handler in arrayOf(timeoutHandler, responseHandler)) {
|
||||
handlers.add(handler)
|
||||
}
|
||||
pipeline.addLast(timeoutHandler, responseHandler)
|
||||
channel.closeFuture().addListener(closeListener)
|
||||
pipeline.addLast(responseHandler)
|
||||
|
||||
|
||||
// Prepare the HTTP request
|
||||
@@ -411,13 +373,14 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
||||
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) {
|
||||
if (profile.compressionEnabled) {
|
||||
set(
|
||||
HttpHeaderNames.ACCEPT_ENCODING,
|
||||
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
|
||||
@@ -436,9 +399,16 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
||||
}
|
||||
}
|
||||
|
||||
// Set headers
|
||||
// Send the request
|
||||
channel.writeAndFlush(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())
|
||||
}
|
@@ -1,10 +1,5 @@
|
||||
package net.woggioni.rbcs.client.impl
|
||||
|
||||
import net.woggioni.rbcs.api.exception.ConfigurationException
|
||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||
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
|
||||
@@ -13,12 +8,17 @@ import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
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
|
||||
|
||||
object Parser {
|
||||
|
||||
fun parse(document: Document): RemoteBuildCacheClient.Configuration {
|
||||
fun parse(document: Document): Configuration {
|
||||
val root = document.documentElement
|
||||
val profiles = mutableMapOf<String, RemoteBuildCacheClient.Configuration.Profile>()
|
||||
val profiles = mutableMapOf<String, Configuration.Profile>()
|
||||
|
||||
for (child in root.asIterable()) {
|
||||
val tagName = child.localName
|
||||
@@ -28,9 +28,15 @@ object Parser {
|
||||
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: RemoteBuildCacheClient.Configuration.Authentication? = null
|
||||
var retryPolicy: RemoteBuildCacheClient.Configuration.RetryPolicy? = null
|
||||
var connection : RemoteBuildCacheClient.Configuration.Connection? = null
|
||||
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" -> {
|
||||
@@ -51,7 +57,7 @@ object Parser {
|
||||
.toList()
|
||||
.toTypedArray()
|
||||
authentication =
|
||||
RemoteBuildCacheClient.Configuration.Authentication.TlsClientAuthenticationCredentials(
|
||||
Configuration.Authentication.TlsClientAuthenticationCredentials(
|
||||
key,
|
||||
certChain
|
||||
)
|
||||
@@ -63,7 +69,7 @@ object Parser {
|
||||
val password = gchild.renderAttribute("password")
|
||||
?: throw ConfigurationException("password attribute is required")
|
||||
authentication =
|
||||
RemoteBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials(
|
||||
Configuration.Authentication.BasicAuthenticationCredentials(
|
||||
username,
|
||||
password
|
||||
)
|
||||
@@ -82,7 +88,7 @@ object Parser {
|
||||
gchild.renderAttribute("exp")
|
||||
?.let(String::toDouble)
|
||||
?: 2.0f
|
||||
retryPolicy = RemoteBuildCacheClient.Configuration.RetryPolicy(
|
||||
retryPolicy = Configuration.RetryPolicy(
|
||||
maxAttempts,
|
||||
initialDelay.toMillis(),
|
||||
exp.toDouble()
|
||||
@@ -90,24 +96,32 @@ object Parser {
|
||||
}
|
||||
|
||||
"connection" -> {
|
||||
val writeTimeout = gchild.renderAttribute("write-timeout")
|
||||
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
|
||||
val readTimeout = gchild.renderAttribute("read-timeout")
|
||||
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
|
||||
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)
|
||||
connection = RemoteBuildCacheClient.Configuration.Connection(
|
||||
readTimeout,
|
||||
writeTimeout,
|
||||
idleTimeout,
|
||||
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")
|
||||
@@ -119,18 +133,19 @@ object Parser {
|
||||
?.let(String::toBoolean)
|
||||
?: true
|
||||
|
||||
profiles[name] = RemoteBuildCacheClient.Configuration.Profile(
|
||||
profiles[name] = Configuration.Profile(
|
||||
uri,
|
||||
connection,
|
||||
authentication,
|
||||
connectionTimeout,
|
||||
maxConnections,
|
||||
compressionEnabled,
|
||||
retryPolicy
|
||||
retryPolicy,
|
||||
trustStore
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return RemoteBuildCacheClient.Configuration(profiles)
|
||||
return Configuration(profiles)
|
||||
}
|
||||
}
|
@@ -15,46 +15,246 @@
|
||||
<xs:complexType name="profileType">
|
||||
<xs:sequence>
|
||||
<xs:choice>
|
||||
<xs:element name="no-auth" type="rbcs-client:noAuthType"/>
|
||||
<xs:element name="basic-auth" type="rbcs-client:basicAuthType"/>
|
||||
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType"/>
|
||||
<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:element name="retry-policy" type="rbcs-client:retryType" minOccurs="0"/>
|
||||
<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:attribute name="base-url" type="xs:anyURI" use="required"/>
|
||||
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
|
||||
<xs:attribute name="connection-timeout" type="xs:duration"/>
|
||||
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
|
||||
<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="read-timeout" type="xs:duration" use="optional" default="PT0S"/>
|
||||
<xs:attribute name="write-timeout" type="xs:duration" use="optional" default="PT0S"/>
|
||||
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S"/>
|
||||
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
|
||||
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
|
||||
<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: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:attribute name="user" type="xs:token" use="required"/>
|
||||
<xs:attribute name="password" type="xs:string" use="required"/>
|
||||
<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: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: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:attribute name="max-attempts" type="xs:positiveInteger" use="required"/>
|
||||
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S"/>
|
||||
<xs:attribute name="exp" type="xs:double" default="2.0"/>
|
||||
<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>
|
||||
|
@@ -2,6 +2,9 @@ package net.woggioni.rbcs.client
|
||||
|
||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||
import io.netty.util.concurrent.EventExecutorGroup
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.stream.Stream
|
||||
import kotlin.random.Random
|
||||
import net.woggioni.rbcs.common.contextLogger
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
@@ -9,9 +12,6 @@ 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 {
|
||||
|
||||
@@ -129,7 +129,7 @@ class RetryTest {
|
||||
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
|
||||
val actualTimestamp = timestamp
|
||||
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
|
||||
Assertions.assertTrue(err < 1e-2)
|
||||
Assertions.assertTrue(err < 0.1)
|
||||
}
|
||||
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
|
||||
/*
|
||||
|
@@ -9,6 +9,8 @@
|
||||
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"/>
|
||||
|
@@ -6,7 +6,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':rbcs-api')
|
||||
implementation catalog.netty.transport
|
||||
implementation catalog.slf4j.api
|
||||
implementation catalog.jwo
|
||||
implementation catalog.netty.buffer
|
||||
|
@@ -2,14 +2,14 @@ package net.woggioni.rbcs.common
|
||||
|
||||
import io.netty.channel.Channel
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.logging.LogManager
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.slf4j.MDC
|
||||
import org.slf4j.event.Level
|
||||
import org.slf4j.spi.LoggingEventBuilder
|
||||
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 <reified T> createLogger() = LoggerFactory.getLogger(T::class.java)
|
||||
|
@@ -1,29 +1,46 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
import net.woggioni.jwo.JWO
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.PKIXParameters
|
||||
import java.security.cert.PKIXRevocationChecker
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.EnumSet
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.jwo.Tuple2
|
||||
|
||||
object RBCS {
|
||||
fun String.toUrl() : URL = URL.of(URI(this), null)
|
||||
fun String.toUrl(): URL = URL.of(URI(this), null)
|
||||
|
||||
const val RBCS_NAMESPACE_URI: String = "urn:net.woggioni.rbcs.server"
|
||||
const val RBCS_PREFIX: String = "rbcs"
|
||||
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
fun ByteArray.toInt(index : Int = 0) : Long {
|
||||
if(index + 4 > size) throw IllegalArgumentException("Not enough bytes to decode a 32 bits integer")
|
||||
var value : Long = 0
|
||||
fun ByteArray.toInt(index: Int = 0): Long {
|
||||
if (index + 4 > size) throw IllegalArgumentException("Not enough bytes to decode a 32 bits integer")
|
||||
var value: Long = 0
|
||||
for (b in index until index + 4) {
|
||||
value = (value shl 8) + (get(b).toInt() and 0xFF)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun ByteArray.toLong(index : Int = 0) : Long {
|
||||
if(index + 8 > size) throw IllegalArgumentException("Not enough bytes to decode a 64 bits long integer")
|
||||
var value : Long = 0
|
||||
fun ByteArray.toLong(index: Int = 0): Long {
|
||||
if (index + 8 > size) throw IllegalArgumentException("Not enough bytes to decode a 64 bits long integer")
|
||||
var value: Long = 0
|
||||
for (b in index until index + 8) {
|
||||
value = (value shl 8) + (get(b).toInt() and 0xFF)
|
||||
}
|
||||
@@ -45,11 +62,18 @@ object RBCS {
|
||||
return JWO.bytesToHex(digest(data, md))
|
||||
}
|
||||
|
||||
fun processCacheKey(key: String, digestAlgorithm: String?) = digestAlgorithm
|
||||
?.let(MessageDigest::getInstance)
|
||||
?.let { md ->
|
||||
digest(key.toByteArray(), md)
|
||||
} ?: key.toByteArray(Charsets.UTF_8)
|
||||
fun processCacheKey(key: String, keyPrefix: String?, digestAlgorithm: String?) : ByteArray {
|
||||
val prefixedKey = if (keyPrefix == null) {
|
||||
key
|
||||
} else {
|
||||
key + keyPrefix
|
||||
}.toByteArray(Charsets.UTF_8)
|
||||
return digestAlgorithm
|
||||
?.let(MessageDigest::getInstance)
|
||||
?.let { md ->
|
||||
digest(prefixedKey, md)
|
||||
} ?: prefixedKey
|
||||
}
|
||||
|
||||
fun Long.toIntOrNull(): Int? {
|
||||
return if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) {
|
||||
@@ -58,4 +82,86 @@ object RBCS {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getFreePort(): Int {
|
||||
var count = 0
|
||||
while (count < 50) {
|
||||
try {
|
||||
ServerSocket(0, 50, InetAddress.getLocalHost()).use { serverSocket ->
|
||||
val candidate = serverSocket.localPort
|
||||
if (candidate > 0) {
|
||||
return candidate
|
||||
} else {
|
||||
throw RuntimeException("Got invalid port number: $candidate")
|
||||
}
|
||||
}
|
||||
} catch (ignored: IOException) {
|
||||
++count
|
||||
}
|
||||
}
|
||||
throw RuntimeException("Error trying to find an open port")
|
||||
}
|
||||
|
||||
fun loadKeystore(file: Path, password: String?): KeyStore {
|
||||
val ext = JWO.splitExtension(file)
|
||||
.map(Tuple2<String, String>::get_2)
|
||||
.orElseThrow {
|
||||
IllegalArgumentException(
|
||||
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
|
||||
)
|
||||
}
|
||||
val keystore = when (ext.substring(1).lowercase()) {
|
||||
"jks" -> KeyStore.getInstance("JKS")
|
||||
"p12", "pfx" -> KeyStore.getInstance("PKCS12")
|
||||
else -> throw IllegalArgumentException(
|
||||
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
|
||||
)
|
||||
}
|
||||
Files.newInputStream(file).use {
|
||||
keystore.load(it, password?.let(String::toCharArray))
|
||||
}
|
||||
return keystore
|
||||
}
|
||||
|
||||
fun getTrustManager(trustStore: KeyStore?, certificateRevocationEnabled: Boolean): X509TrustManager {
|
||||
return if (trustStore != null) {
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val validator = CertPathValidator.getInstance("PKIX").apply {
|
||||
val rc = revocationChecker as PKIXRevocationChecker
|
||||
rc.options = EnumSet.of(
|
||||
PKIXRevocationChecker.Option.NO_FALLBACK
|
||||
)
|
||||
}
|
||||
val params = PKIXParameters(trustStore).apply {
|
||||
isRevocationEnabled = certificateRevocationEnabled
|
||||
}
|
||||
object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
|
||||
val clientCertificateChain = certificateFactory.generateCertPath(chain.toList())
|
||||
try {
|
||||
validator.validate(clientCertificateChain, params)
|
||||
} catch (ex: CertPathValidatorException) {
|
||||
throw CertificateException(ex)
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
private val acceptedIssuers = trustStore.aliases().asSequence()
|
||||
.filter(trustStore::isCertificateEntry)
|
||||
.map(trustStore::getCertificate)
|
||||
.map { it as X509Certificate }
|
||||
.toList()
|
||||
.toTypedArray()
|
||||
|
||||
override fun getAcceptedIssuers() = acceptedIssuers
|
||||
}
|
||||
} else {
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }
|
||||
.single() as X509TrustManager
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +1,5 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
import net.woggioni.jwo.JWO
|
||||
import org.slf4j.event.Level
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.NodeList
|
||||
import org.xml.sax.SAXNotRecognizedException
|
||||
import org.xml.sax.SAXNotSupportedException
|
||||
import org.xml.sax.SAXParseException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.URL
|
||||
@@ -25,7 +16,16 @@ import javax.xml.transform.stream.StreamResult
|
||||
import javax.xml.transform.stream.StreamSource
|
||||
import javax.xml.validation.Schema
|
||||
import javax.xml.validation.SchemaFactory
|
||||
import net.woggioni.jwo.JWO
|
||||
import org.slf4j.event.Level
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.NodeList
|
||||
import org.xml.sax.ErrorHandler as ErrHandler
|
||||
import org.xml.sax.SAXNotRecognizedException
|
||||
import org.xml.sax.SAXNotSupportedException
|
||||
import org.xml.sax.SAXParseException
|
||||
|
||||
|
||||
class NodeListIterator(private val nodeList: NodeList) : Iterator<Node> {
|
||||
|
@@ -1,14 +1,14 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
import java.security.Provider
|
||||
import java.security.Security
|
||||
import java.util.Base64
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.EnumSource
|
||||
import java.security.Provider
|
||||
import java.security.Security
|
||||
import java.util.Base64
|
||||
|
||||
|
||||
class PasswordHashingTest {
|
||||
|
@@ -22,7 +22,7 @@ The plugins currently supports the following configuration attributes:
|
||||
- `digest`: digest algorithm to use on the key before submission
|
||||
to memcache (optional, no digest is applied if omitted)
|
||||
- `compression`: compression algorithm to apply to cache values before,
|
||||
currently only `deflate` is supported (optionla, if omitted compression is disabled)
|
||||
currently only `deflate` is supported (optional, if omitted compression is disabled)
|
||||
- `compression-level`: compression level to use, deflate supports compression levels from 1 to 9,
|
||||
where 1 is for fast compression at the expense of speed (optional, 6 is used if omitted)
|
||||
```xml
|
||||
@@ -30,15 +30,14 @@ The plugins currently supports the following configuration attributes:
|
||||
<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.xsd"
|
||||
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"
|
||||
>
|
||||
...
|
||||
<cache xs:type="rbcs-memcache:memcacheCacheType"
|
||||
max-age="P7D"
|
||||
digest="SHA-256"
|
||||
compression-mode="deflate"
|
||||
compression-level="6"
|
||||
chunk-size="0x10000">
|
||||
compression-level="6">
|
||||
<server host="127.0.0.1" port="11211" max-connections="256"/>
|
||||
<server host="127.0.0.1" port="11212" max-connections="256"/>
|
||||
</cache>
|
||||
|
@@ -1,30 +1,35 @@
|
||||
package net.woggioni.rbcs.server.memcache
|
||||
|
||||
import io.netty.channel.ChannelFactory
|
||||
import io.netty.channel.ChannelHandler
|
||||
import io.netty.channel.EventLoopGroup
|
||||
import io.netty.channel.pool.FixedChannelPool
|
||||
import io.netty.channel.socket.DatagramChannel
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import net.woggioni.rbcs.api.CacheHandlerFactory
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.HostAndPort
|
||||
import net.woggioni.rbcs.server.memcache.client.MemcacheClient
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import net.woggioni.rbcs.api.CacheHandler
|
||||
import net.woggioni.rbcs.api.CacheHandlerFactory
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.HostAndPort
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.server.memcache.client.MemcacheClient
|
||||
|
||||
data class MemcacheCacheConfiguration(
|
||||
val servers: List<Server>,
|
||||
val maxAge: Duration = Duration.ofDays(1),
|
||||
val keyPrefix : String? = null,
|
||||
val digestAlgorithm: String? = null,
|
||||
val compressionMode: CompressionMode? = null,
|
||||
val compressionLevel: Int,
|
||||
val chunkSize: Int
|
||||
) : Configuration.Cache {
|
||||
|
||||
companion object {
|
||||
private val log = createLogger<MemcacheCacheConfiguration>()
|
||||
}
|
||||
|
||||
enum class CompressionMode {
|
||||
/**
|
||||
* Deflate mode
|
||||
@@ -43,22 +48,24 @@ data class MemcacheCacheConfiguration(
|
||||
private val connectionPoolMap = ConcurrentHashMap<HostAndPort, FixedChannelPool>()
|
||||
|
||||
override fun newHandler(
|
||||
cfg : Configuration,
|
||||
eventLoop: EventLoopGroup,
|
||||
socketChannelFactory: ChannelFactory<SocketChannel>,
|
||||
datagramChannelFactory: ChannelFactory<DatagramChannel>
|
||||
): ChannelHandler {
|
||||
datagramChannelFactory: ChannelFactory<DatagramChannel>,
|
||||
): CacheHandler {
|
||||
return MemcacheCacheHandler(
|
||||
MemcacheClient(
|
||||
this@MemcacheCacheConfiguration.servers,
|
||||
chunkSize,
|
||||
cfg.connection.chunkSize,
|
||||
eventLoop,
|
||||
socketChannelFactory,
|
||||
connectionPoolMap
|
||||
),
|
||||
keyPrefix,
|
||||
digestAlgorithm,
|
||||
compressionMode != null,
|
||||
compressionLevel,
|
||||
chunkSize,
|
||||
cfg.connection.chunkSize,
|
||||
maxAge
|
||||
)
|
||||
}
|
||||
@@ -69,15 +76,19 @@ data class MemcacheCacheConfiguration(
|
||||
val pools = connectionPoolMap.values.toList()
|
||||
val npools = pools.size
|
||||
val finished = AtomicInteger(0)
|
||||
pools.forEach { pool ->
|
||||
pool.closeAsync().addListener {
|
||||
if (!it.isSuccess) {
|
||||
failure.compareAndSet(null, it.cause())
|
||||
}
|
||||
if(finished.incrementAndGet() == npools) {
|
||||
when(val ex = failure.get()) {
|
||||
null -> complete(null)
|
||||
else -> completeExceptionally(ex)
|
||||
if (pools.isEmpty()) {
|
||||
complete(null)
|
||||
} else {
|
||||
pools.forEach { pool ->
|
||||
pool.closeAsync().addListener {
|
||||
if (!it.isSuccess) {
|
||||
failure.compareAndSet(null, it.cause())
|
||||
}
|
||||
if (finished.incrementAndGet() == npools) {
|
||||
when (val ex = failure.get()) {
|
||||
null -> complete(null)
|
||||
else -> completeExceptionally(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ package net.woggioni.rbcs.server.memcache
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufAllocator
|
||||
import io.netty.buffer.CompositeByteBuf
|
||||
import io.netty.channel.Channel as NettyChannel
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.handler.codec.memcache.DefaultLastMemcacheContent
|
||||
import io.netty.handler.codec.memcache.DefaultMemcacheContent
|
||||
import io.netty.handler.codec.memcache.LastMemcacheContent
|
||||
@@ -13,6 +13,22 @@ import io.netty.handler.codec.memcache.binary.BinaryMemcacheOpcodes
|
||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse
|
||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponseStatus
|
||||
import io.netty.handler.codec.memcache.binary.DefaultBinaryMemcacheRequest
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.ReadableByteChannel
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterOutputStream
|
||||
import net.woggioni.rbcs.api.CacheHandler
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.api.exception.ContentTooLargeException
|
||||
import net.woggioni.rbcs.api.message.CacheMessage
|
||||
@@ -34,31 +50,16 @@ import net.woggioni.rbcs.common.trace
|
||||
import net.woggioni.rbcs.server.memcache.client.MemcacheClient
|
||||
import net.woggioni.rbcs.server.memcache.client.MemcacheRequestController
|
||||
import net.woggioni.rbcs.server.memcache.client.MemcacheResponseHandler
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.ReadableByteChannel
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterOutputStream
|
||||
import io.netty.channel.Channel as NettyChannel
|
||||
|
||||
class MemcacheCacheHandler(
|
||||
private val client: MemcacheClient,
|
||||
private val keyPrefix: String?,
|
||||
private val digestAlgorithm: String?,
|
||||
private val compressionEnabled: Boolean,
|
||||
private val compressionLevel: Int,
|
||||
private val chunkSize: Int,
|
||||
private val maxAge: Duration
|
||||
) : SimpleChannelInboundHandler<CacheMessage>() {
|
||||
) : CacheHandler() {
|
||||
companion object {
|
||||
private val log = createLogger<MemcacheCacheHandler>()
|
||||
|
||||
@@ -69,10 +70,14 @@ class MemcacheCacheHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private interface InProgressRequest {
|
||||
|
||||
}
|
||||
|
||||
private inner class InProgressGetRequest(
|
||||
private val key: String,
|
||||
val key: String,
|
||||
private val ctx: ChannelHandlerContext
|
||||
) {
|
||||
) : InProgressRequest {
|
||||
private val acc = ctx.alloc().compositeBuffer()
|
||||
private val chunk = ctx.alloc().compositeBuffer()
|
||||
private val outputStream = ByteBufOutputStream(chunk).let {
|
||||
@@ -98,32 +103,35 @@ class MemcacheCacheHandler(
|
||||
acc.retain()
|
||||
it.readObject() as CacheValueMetadata
|
||||
}
|
||||
ctx.writeAndFlush(CacheValueFoundResponse(key, metadata))
|
||||
log.trace(ctx) {
|
||||
"Sending response from cache"
|
||||
}
|
||||
sendMessageAndFlush(ctx, CacheValueFoundResponse(key, metadata))
|
||||
responseSent = true
|
||||
acc.readerIndex(Int.SIZE_BYTES + mSize)
|
||||
}
|
||||
if (responseSent) {
|
||||
acc.readBytes(outputStream, acc.readableBytes())
|
||||
if(acc.readableBytes() >= chunkSize) {
|
||||
if (acc.readableBytes() >= chunkSize) {
|
||||
flush(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun flush(last : Boolean) {
|
||||
private fun flush(last: Boolean) {
|
||||
val toSend = extractChunk(chunk, ctx.alloc())
|
||||
val msg = if(last) {
|
||||
val msg = if (last) {
|
||||
log.trace(ctx) {
|
||||
"Sending last chunk to client on channel ${ctx.channel().id().asShortText()}"
|
||||
"Sending last chunk to client"
|
||||
}
|
||||
LastCacheContent(toSend)
|
||||
} else {
|
||||
log.trace(ctx) {
|
||||
"Sending chunk to client on channel ${ctx.channel().id().asShortText()}"
|
||||
"Sending chunk to client"
|
||||
}
|
||||
CacheContent(toSend)
|
||||
}
|
||||
ctx.writeAndFlush(msg)
|
||||
sendMessageAndFlush(ctx, msg)
|
||||
}
|
||||
|
||||
fun commit() {
|
||||
@@ -141,14 +149,14 @@ class MemcacheCacheHandler(
|
||||
}
|
||||
|
||||
private inner class InProgressPutRequest(
|
||||
private val ch : NettyChannel,
|
||||
metadata : CacheValueMetadata,
|
||||
val digest : ByteBuf,
|
||||
private val ch: NettyChannel,
|
||||
metadata: CacheValueMetadata,
|
||||
val digest: ByteBuf,
|
||||
val requestController: CompletableFuture<MemcacheRequestController>,
|
||||
private val alloc: ByteBufAllocator
|
||||
) {
|
||||
) : InProgressRequest {
|
||||
private var totalSize = 0
|
||||
private var tmpFile : FileChannel? = null
|
||||
private var tmpFile: FileChannel? = null
|
||||
private val accumulator = alloc.compositeBuffer()
|
||||
private val stream = ByteBufOutputStream(accumulator).let {
|
||||
if (compressionEnabled) {
|
||||
@@ -175,7 +183,7 @@ class MemcacheCacheHandler(
|
||||
tmpFile?.let {
|
||||
flushToDisk(it, accumulator)
|
||||
}
|
||||
if(accumulator.readableBytes() > 0x100000) {
|
||||
if (accumulator.readableBytes() > 0x100000) {
|
||||
log.debug(ch) {
|
||||
"Entry is too big, buffering it into a file"
|
||||
}
|
||||
@@ -192,18 +200,18 @@ class MemcacheCacheHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushToDisk(fc : FileChannel, buf : CompositeByteBuf) {
|
||||
private fun flushToDisk(fc: FileChannel, buf: CompositeByteBuf) {
|
||||
val chunk = extractChunk(buf, alloc)
|
||||
fc.write(chunk.nioBuffer())
|
||||
chunk.release()
|
||||
}
|
||||
|
||||
fun commit() : Pair<Int, ReadableByteChannel> {
|
||||
fun commit(): Pair<Int, ReadableByteChannel> {
|
||||
digest.release()
|
||||
accumulator.retain()
|
||||
stream.close()
|
||||
val fileChannel = tmpFile
|
||||
return if(fileChannel != null) {
|
||||
return if (fileChannel != null) {
|
||||
flushToDisk(fileChannel, accumulator)
|
||||
accumulator.release()
|
||||
fileChannel.position(0)
|
||||
@@ -224,8 +232,7 @@ class MemcacheCacheHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private var inProgressPutRequest: InProgressPutRequest? = null
|
||||
private var inProgressGetRequest: InProgressGetRequest? = null
|
||||
private var inProgressRequest: InProgressRequest? = null
|
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
|
||||
when (msg) {
|
||||
@@ -242,7 +249,7 @@ class MemcacheCacheHandler(
|
||||
"Fetching ${msg.key} from memcache"
|
||||
}
|
||||
val key = ctx.alloc().buffer().also {
|
||||
it.writeBytes(processCacheKey(msg.key, digestAlgorithm))
|
||||
it.writeBytes(processCacheKey(msg.key, keyPrefix, digestAlgorithm))
|
||||
}
|
||||
val responseHandler = object : MemcacheResponseHandler {
|
||||
override fun responseReceived(response: BinaryMemcacheResponse) {
|
||||
@@ -252,32 +259,39 @@ class MemcacheCacheHandler(
|
||||
log.debug(ctx) {
|
||||
"Cache hit for key ${msg.key} on memcache"
|
||||
}
|
||||
inProgressGetRequest = InProgressGetRequest(msg.key, ctx)
|
||||
inProgressRequest = InProgressGetRequest(msg.key, ctx)
|
||||
}
|
||||
|
||||
BinaryMemcacheResponseStatus.KEY_ENOENT -> {
|
||||
log.debug(ctx) {
|
||||
"Cache miss for key ${msg.key} on memcache"
|
||||
}
|
||||
ctx.writeAndFlush(CacheValueNotFoundResponse())
|
||||
sendMessageAndFlush(ctx, CacheValueNotFoundResponse(msg.key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentReceived(content: MemcacheContent) {
|
||||
log.trace(ctx) {
|
||||
"${if(content is LastMemcacheContent) "Last chunk" else "Chunk"} of ${content.content().readableBytes()} bytes received from memcache for key ${msg.key}"
|
||||
"${if (content is LastMemcacheContent) "Last chunk" else "Chunk"} of ${
|
||||
content.content().readableBytes()
|
||||
} bytes received from memcache for key ${msg.key}"
|
||||
}
|
||||
inProgressGetRequest?.write(content.content())
|
||||
if (content is LastMemcacheContent) {
|
||||
inProgressGetRequest?.commit()
|
||||
(inProgressRequest as? InProgressGetRequest)?.let { inProgressGetRequest ->
|
||||
inProgressGetRequest.write(content.content())
|
||||
if (content is LastMemcacheContent) {
|
||||
inProgressRequest = null
|
||||
inProgressGetRequest.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ex: Throwable) {
|
||||
inProgressGetRequest?.let {
|
||||
inProgressGetRequest = null
|
||||
it.rollback()
|
||||
(inProgressRequest as? InProgressGetRequest).let { inProgressGetRequest ->
|
||||
inProgressGetRequest?.let {
|
||||
inProgressRequest = null
|
||||
it.rollback()
|
||||
}
|
||||
}
|
||||
this@MemcacheCacheHandler.exceptionCaught(ctx, ex)
|
||||
}
|
||||
@@ -290,12 +304,13 @@ class MemcacheCacheHandler(
|
||||
setOpcode(BinaryMemcacheOpcodes.GET)
|
||||
}
|
||||
requestHandle.sendRequest(request)
|
||||
requestHandle.sendContent(LastMemcacheContent.EMPTY_LAST_CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
|
||||
val key = ctx.alloc().buffer().also {
|
||||
it.writeBytes(processCacheKey(msg.key, digestAlgorithm))
|
||||
it.writeBytes(processCacheKey(msg.key, keyPrefix, digestAlgorithm))
|
||||
}
|
||||
val responseHandler = object : MemcacheResponseHandler {
|
||||
override fun responseReceived(response: BinaryMemcacheResponse) {
|
||||
@@ -305,8 +320,9 @@ class MemcacheCacheHandler(
|
||||
log.debug(ctx) {
|
||||
"Inserted key ${msg.key} into memcache"
|
||||
}
|
||||
ctx.writeAndFlush(CachePutResponse(msg.key))
|
||||
sendMessageAndFlush(ctx, CachePutResponse(msg.key))
|
||||
}
|
||||
|
||||
else -> this@MemcacheCacheHandler.exceptionCaught(ctx, MemcacheException(status))
|
||||
}
|
||||
}
|
||||
@@ -323,86 +339,103 @@ class MemcacheCacheHandler(
|
||||
this@MemcacheCacheHandler.exceptionCaught(ctx, ex)
|
||||
}
|
||||
}
|
||||
inProgressPutRequest = InProgressPutRequest(ctx.channel(), msg.metadata, key, requestController, ctx.alloc())
|
||||
inProgressRequest = InProgressPutRequest(ctx.channel(), msg.metadata, key, requestController, ctx.alloc())
|
||||
}
|
||||
|
||||
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
|
||||
inProgressPutRequest?.let { request ->
|
||||
log.trace(ctx) {
|
||||
"Received chunk of ${msg.content().readableBytes()} bytes for memcache"
|
||||
val request = inProgressRequest
|
||||
when (request) {
|
||||
is InProgressPutRequest -> {
|
||||
log.trace(ctx) {
|
||||
"Received chunk of ${msg.content().readableBytes()} bytes for memcache"
|
||||
}
|
||||
request.write(msg.content())
|
||||
}
|
||||
|
||||
is InProgressGetRequest -> {
|
||||
msg.release()
|
||||
}
|
||||
request.write(msg.content())
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
|
||||
inProgressPutRequest?.let { request ->
|
||||
inProgressPutRequest = null
|
||||
log.trace(ctx) {
|
||||
"Received last chunk of ${msg.content().readableBytes()} bytes for memcache"
|
||||
}
|
||||
request.write(msg.content())
|
||||
val key = request.digest.retainedDuplicate()
|
||||
val (payloadSize, payloadSource) = request.commit()
|
||||
val extras = ctx.alloc().buffer(8, 8)
|
||||
extras.writeInt(0)
|
||||
extras.writeInt(encodeExpiry(maxAge))
|
||||
val totalBodyLength = request.digest.readableBytes() + extras.readableBytes() + payloadSize
|
||||
request.requestController.whenComplete { requestController, ex ->
|
||||
if(ex == null) {
|
||||
log.trace(ctx) {
|
||||
"Sending SET request to memcache"
|
||||
}
|
||||
requestController.sendRequest(DefaultBinaryMemcacheRequest().apply {
|
||||
setOpcode(BinaryMemcacheOpcodes.SET)
|
||||
setKey(key)
|
||||
setExtras(extras)
|
||||
setTotalBodyLength(totalBodyLength)
|
||||
})
|
||||
log.trace(ctx) {
|
||||
"Sending request payload to memcache"
|
||||
}
|
||||
payloadSource.use { source ->
|
||||
val bb = ByteBuffer.allocate(chunkSize)
|
||||
while (true) {
|
||||
val read = source.read(bb)
|
||||
bb.limit()
|
||||
if(read >= 0 && bb.position() < chunkSize && bb.hasRemaining()) {
|
||||
continue
|
||||
}
|
||||
val chunk = ctx.alloc().buffer(chunkSize)
|
||||
bb.flip()
|
||||
chunk.writeBytes(bb)
|
||||
bb.clear()
|
||||
log.trace(ctx) {
|
||||
"Sending ${chunk.readableBytes()} bytes chunk to memcache"
|
||||
}
|
||||
if(read < 0) {
|
||||
requestController.sendContent(DefaultLastMemcacheContent(chunk))
|
||||
break
|
||||
} else {
|
||||
requestController.sendContent(DefaultMemcacheContent(chunk))
|
||||
val request = inProgressRequest
|
||||
when (request) {
|
||||
is InProgressPutRequest -> {
|
||||
inProgressRequest = null
|
||||
log.trace(ctx) {
|
||||
"Received last chunk of ${msg.content().readableBytes()} bytes for memcache"
|
||||
}
|
||||
request.write(msg.content())
|
||||
val key = request.digest.retainedDuplicate()
|
||||
val (payloadSize, payloadSource) = request.commit()
|
||||
val extras = ctx.alloc().buffer(8, 8)
|
||||
extras.writeInt(0)
|
||||
extras.writeInt(encodeExpiry(maxAge))
|
||||
val totalBodyLength = request.digest.readableBytes() + extras.readableBytes() + payloadSize
|
||||
log.trace(ctx) {
|
||||
"Trying to send SET request to memcache"
|
||||
}
|
||||
request.requestController.whenComplete { requestController, ex ->
|
||||
if (ex == null) {
|
||||
log.trace(ctx) {
|
||||
"Sending SET request to memcache"
|
||||
}
|
||||
requestController.sendRequest(DefaultBinaryMemcacheRequest().apply {
|
||||
setOpcode(BinaryMemcacheOpcodes.SET)
|
||||
setKey(key)
|
||||
setExtras(extras)
|
||||
setTotalBodyLength(totalBodyLength)
|
||||
})
|
||||
log.trace(ctx) {
|
||||
"Sending request payload to memcache"
|
||||
}
|
||||
payloadSource.use { source ->
|
||||
val bb = ByteBuffer.allocate(chunkSize)
|
||||
while (true) {
|
||||
val read = source.read(bb)
|
||||
bb.limit()
|
||||
if (read >= 0 && bb.position() < chunkSize && bb.hasRemaining()) {
|
||||
continue
|
||||
}
|
||||
val chunk = ctx.alloc().buffer(chunkSize)
|
||||
bb.flip()
|
||||
chunk.writeBytes(bb)
|
||||
bb.clear()
|
||||
log.trace(ctx) {
|
||||
"Sending ${chunk.readableBytes()} bytes chunk to memcache"
|
||||
}
|
||||
if (read < 0) {
|
||||
requestController.sendContent(DefaultLastMemcacheContent(chunk))
|
||||
break
|
||||
} else {
|
||||
requestController.sendContent(DefaultMemcacheContent(chunk))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payloadSource.close()
|
||||
}
|
||||
} else {
|
||||
payloadSource.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
inProgressGetRequest?.let {
|
||||
inProgressGetRequest = null
|
||||
it.rollback()
|
||||
}
|
||||
inProgressPutRequest?.let {
|
||||
inProgressPutRequest = null
|
||||
it.requestController.thenAccept { controller ->
|
||||
controller.exceptionCaught(cause)
|
||||
val request = inProgressRequest
|
||||
when (request) {
|
||||
is InProgressPutRequest -> {
|
||||
inProgressRequest = null
|
||||
request.requestController.thenAccept { controller ->
|
||||
controller.exceptionCaught(cause)
|
||||
}
|
||||
request.rollback()
|
||||
}
|
||||
|
||||
is InProgressGetRequest -> {
|
||||
inProgressRequest = null
|
||||
request.rollback()
|
||||
}
|
||||
it.rollback()
|
||||
}
|
||||
super.exceptionCaught(ctx, cause)
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package net.woggioni.rbcs.server.memcache
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
import net.woggioni.rbcs.api.CacheProvider
|
||||
import net.woggioni.rbcs.api.exception.ConfigurationException
|
||||
import net.woggioni.rbcs.common.HostAndPort
|
||||
@@ -9,8 +11,6 @@ import net.woggioni.rbcs.common.Xml.Companion.asIterable
|
||||
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
|
||||
class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
||||
@@ -28,9 +28,6 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
||||
val maxAge = el.renderAttribute("max-age")
|
||||
?.let(Duration::parse)
|
||||
?: Duration.ofDays(1)
|
||||
val chunkSize = el.renderAttribute("chunk-size")
|
||||
?.let(Integer::decode)
|
||||
?: 0x10000
|
||||
val compressionLevel = el.renderAttribute("compression-level")
|
||||
?.let(Integer::decode)
|
||||
?: -1
|
||||
@@ -41,6 +38,7 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
||||
else -> MemcacheCacheConfiguration.CompressionMode.DEFLATE
|
||||
}
|
||||
}
|
||||
val keyPrefix = el.renderAttribute("key-prefix")
|
||||
val digestAlgorithm = el.renderAttribute("digest")
|
||||
for (child in el.asIterable()) {
|
||||
when (child.nodeName) {
|
||||
@@ -57,14 +55,13 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MemcacheCacheConfiguration(
|
||||
servers,
|
||||
maxAge,
|
||||
keyPrefix,
|
||||
digestAlgorithm,
|
||||
compressionMode,
|
||||
compressionLevel,
|
||||
chunkSize
|
||||
compressionLevel
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,9 +79,12 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
||||
}
|
||||
attr("max-connections", server.maxConnections.toString())
|
||||
}
|
||||
|
||||
}
|
||||
attr("max-age", maxAge.toString())
|
||||
attr("chunk-size", chunkSize.toString())
|
||||
keyPrefix?.let {
|
||||
attr("key-prefix", it)
|
||||
}
|
||||
digestAlgorithm?.let { digestAlgorithm ->
|
||||
attr("digest", digestAlgorithm)
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import io.netty.channel.ChannelPipeline
|
||||
import io.netty.channel.EventLoopGroup
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.channel.pool.AbstractChannelPoolHandler
|
||||
import io.netty.channel.pool.ChannelPool
|
||||
import io.netty.channel.pool.FixedChannelPool
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.handler.codec.memcache.LastMemcacheContent
|
||||
@@ -21,17 +20,17 @@ import io.netty.handler.codec.memcache.MemcacheObject
|
||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec
|
||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheRequest
|
||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse
|
||||
import io.netty.util.concurrent.Future as NettyFuture
|
||||
import io.netty.util.concurrent.GenericFutureListener
|
||||
import net.woggioni.rbcs.common.HostAndPort
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.warn
|
||||
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
|
||||
import net.woggioni.rbcs.server.memcache.MemcacheCacheHandler
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import io.netty.util.concurrent.Future as NettyFuture
|
||||
import net.woggioni.rbcs.common.HostAndPort
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.trace
|
||||
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
|
||||
import net.woggioni.rbcs.server.memcache.MemcacheCacheHandler
|
||||
|
||||
|
||||
class MemcacheClient(
|
||||
@@ -94,18 +93,6 @@ class MemcacheClient(
|
||||
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
||||
override fun operationComplete(channelFuture: NettyFuture<Channel>) {
|
||||
if (channelFuture.isSuccess) {
|
||||
|
||||
var requestSent = false
|
||||
var requestBodySent = false
|
||||
var requestFinished = false
|
||||
var responseReceived = false
|
||||
var responseBodyReceived = false
|
||||
var responseFinished = false
|
||||
var requestBodySize = 0
|
||||
var requestBodyBytesSent = 0
|
||||
|
||||
|
||||
|
||||
val channel = channelFuture.now
|
||||
var connectionClosedByTheRemoteServer = true
|
||||
val closeCallback = {
|
||||
@@ -113,14 +100,7 @@ class MemcacheClient(
|
||||
val ex = IOException("The memcache server closed the connection")
|
||||
val completed = response.completeExceptionally(ex)
|
||||
if(!completed) responseHandler.exceptionCaught(ex)
|
||||
log.warn {
|
||||
"RequestSent: $requestSent, RequestBodySent: $requestBodySent, " +
|
||||
"RequestFinished: $requestFinished, ResponseReceived: $responseReceived, " +
|
||||
"ResponseBodyReceived: $responseBodyReceived, ResponseFinished: $responseFinished, " +
|
||||
"RequestBodySize: $requestBodySize, RequestBodyBytesSent: $requestBodyBytesSent"
|
||||
}
|
||||
}
|
||||
pool.release(channel)
|
||||
}
|
||||
val closeListener = ChannelFutureListener {
|
||||
closeCallback()
|
||||
@@ -140,18 +120,14 @@ class MemcacheClient(
|
||||
when (msg) {
|
||||
is BinaryMemcacheResponse -> {
|
||||
responseHandler.responseReceived(msg)
|
||||
responseReceived = true
|
||||
}
|
||||
|
||||
is LastMemcacheContent -> {
|
||||
responseFinished = true
|
||||
responseHandler.contentReceived(msg)
|
||||
pipeline.remove(this)
|
||||
pool.release(channel)
|
||||
}
|
||||
|
||||
is MemcacheContent -> {
|
||||
responseBodyReceived = true
|
||||
responseHandler.contentReceived(msg)
|
||||
}
|
||||
}
|
||||
@@ -165,35 +141,43 @@ class MemcacheClient(
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
connectionClosedByTheRemoteServer = false
|
||||
ctx.close()
|
||||
pool.release(channel)
|
||||
responseHandler.exceptionCaught(cause)
|
||||
}
|
||||
}
|
||||
|
||||
channel.pipeline()
|
||||
.addLast("client-handler", handler)
|
||||
channel.pipeline().addLast(handler)
|
||||
response.complete(object : MemcacheRequestController {
|
||||
private var channelReleased = false
|
||||
|
||||
override fun sendRequest(request: BinaryMemcacheRequest) {
|
||||
requestBodySize = request.totalBodyLength() - request.keyLength() - request.extrasLength()
|
||||
channel.writeAndFlush(request)
|
||||
requestSent = true
|
||||
}
|
||||
|
||||
override fun sendContent(content: MemcacheContent) {
|
||||
val size = content.content().readableBytes()
|
||||
channel.writeAndFlush(content).addListener {
|
||||
requestBodyBytesSent += size
|
||||
requestBodySent = true
|
||||
if(content is LastMemcacheContent) {
|
||||
requestFinished = true
|
||||
if(!channelReleased) {
|
||||
pool.release(channel)
|
||||
channelReleased = true
|
||||
log.trace(channel) {
|
||||
"Channel released"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ex: Throwable) {
|
||||
log.warn(ex.message, ex)
|
||||
connectionClosedByTheRemoteServer = false
|
||||
channel.close()
|
||||
if(!channelReleased) {
|
||||
pool.release(channel)
|
||||
channelReleased = true
|
||||
log.trace(channel) {
|
||||
"Channel released"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
@@ -4,7 +4,7 @@
|
||||
xmlns:rbcs="urn:net.woggioni.rbcs.server"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
|
||||
<xs:import schemaLocation="jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd" namespace="urn:net.woggioni.rbcs.server"/>
|
||||
<xs:import schemaLocation="jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd" namespace="urn:net.woggioni.rbcs.server"/>
|
||||
|
||||
<xs:complexType name="memcacheServerType">
|
||||
<xs:attribute name="host" type="xs:token" use="required"/>
|
||||
@@ -21,6 +21,14 @@
|
||||
</xs:sequence>
|
||||
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
|
||||
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
|
||||
<xs:attribute name="key-prefix" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Prepend this string to all the keys inserted in memcache,
|
||||
useful in case the caching backend is shared with other applications
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="digest" type="xs:token"/>
|
||||
<xs:attribute name="compression-mode" type="rbcs-memcache:compressionType"/>
|
||||
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/>
|
||||
|
@@ -2,12 +2,12 @@ package net.woggioni.rbcs.server.memcache.client
|
||||
|
||||
import io.netty.buffer.ByteBufUtil
|
||||
import io.netty.buffer.Unpooled
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import kotlin.random.Random
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ByteBufferTest {
|
||||
|
||||
|
@@ -3,27 +3,27 @@ import net.woggioni.rbcs.server.cache.FileSystemCacheProvider;
|
||||
import net.woggioni.rbcs.server.cache.InMemoryCacheProvider;
|
||||
|
||||
module net.woggioni.rbcs.server {
|
||||
requires java.sql;
|
||||
requires java.xml;
|
||||
requires java.logging;
|
||||
requires java.naming;
|
||||
requires kotlin.stdlib;
|
||||
requires io.netty.buffer;
|
||||
requires io.netty.transport;
|
||||
requires io.netty.codec.http;
|
||||
requires io.netty.common;
|
||||
requires io.netty.handler;
|
||||
requires io.netty.codec;
|
||||
requires org.slf4j;
|
||||
requires net.woggioni.jwo;
|
||||
requires net.woggioni.rbcs.common;
|
||||
requires net.woggioni.rbcs.api;
|
||||
requires io.netty.codec.compression;
|
||||
requires io.netty.transport;
|
||||
requires io.netty.buffer;
|
||||
requires io.netty.common;
|
||||
requires io.netty.codec;
|
||||
requires org.slf4j;
|
||||
|
||||
exports net.woggioni.rbcs.server;
|
||||
|
||||
opens net.woggioni.rbcs.server;
|
||||
opens net.woggioni.rbcs.server.schema;
|
||||
|
||||
|
||||
uses CacheProvider;
|
||||
provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider;
|
||||
}
|
@@ -11,7 +11,8 @@ import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
import io.netty.channel.ChannelInitializer
|
||||
import io.netty.channel.ChannelOption
|
||||
import io.netty.channel.ChannelPromise
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.channel.MultiThreadIoEventLoopGroup
|
||||
import io.netty.channel.nio.NioIoHandler
|
||||
import io.netty.channel.socket.DatagramChannel
|
||||
import io.netty.channel.socket.ServerSocketChannel
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
@@ -21,6 +22,7 @@ import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import io.netty.handler.codec.compression.CompressionOptions
|
||||
import io.netty.handler.codec.http.DefaultHttpContent
|
||||
import io.netty.handler.codec.http.HttpContentCompressor
|
||||
import io.netty.handler.codec.http.HttpDecoderConfig
|
||||
import io.netty.handler.codec.http.HttpHeaderNames
|
||||
import io.netty.handler.codec.http.HttpRequest
|
||||
import io.netty.handler.codec.http.HttpServerCodec
|
||||
@@ -33,37 +35,11 @@ import io.netty.handler.timeout.IdleState
|
||||
import io.netty.handler.timeout.IdleStateEvent
|
||||
import io.netty.handler.timeout.IdleStateHandler
|
||||
import io.netty.util.AttributeKey
|
||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||
import io.netty.util.concurrent.EventExecutorGroup
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.jwo.Tuple2
|
||||
import net.woggioni.rbcs.api.AsyncCloseable
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.exception.ConfigurationException
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||
import net.woggioni.rbcs.common.RBCS.toUrl
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.common.info
|
||||
import net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator
|
||||
import net.woggioni.rbcs.server.auth.Authorizer
|
||||
import net.woggioni.rbcs.server.auth.ClientCertificateValidator
|
||||
import net.woggioni.rbcs.server.auth.RoleAuthorizer
|
||||
import net.woggioni.rbcs.server.configuration.Parser
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
import net.woggioni.rbcs.server.exception.ExceptionHandler
|
||||
import net.woggioni.rbcs.server.handler.MaxRequestSizeHandler
|
||||
import net.woggioni.rbcs.server.handler.ServerHandler
|
||||
import net.woggioni.rbcs.server.handler.TraceHandler
|
||||
import net.woggioni.rbcs.server.throttling.BucketManager
|
||||
import net.woggioni.rbcs.server.throttling.ThrottlingHandler
|
||||
import java.io.OutputStream
|
||||
import java.net.InetSocketAddress
|
||||
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
|
||||
@@ -78,6 +54,29 @@ import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
import javax.naming.ldap.LdapName
|
||||
import javax.net.ssl.SSLPeerUnverifiedException
|
||||
import net.woggioni.rbcs.api.AsyncCloseable
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.exception.ConfigurationException
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||
import net.woggioni.rbcs.common.RBCS.getTrustManager
|
||||
import net.woggioni.rbcs.common.RBCS.loadKeystore
|
||||
import net.woggioni.rbcs.common.RBCS.toUrl
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.common.info
|
||||
import net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator
|
||||
import net.woggioni.rbcs.server.auth.Authorizer
|
||||
import net.woggioni.rbcs.server.auth.RoleAuthorizer
|
||||
import net.woggioni.rbcs.server.configuration.Parser
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
import net.woggioni.rbcs.server.exception.ExceptionHandler
|
||||
import net.woggioni.rbcs.server.handler.MaxRequestSizeHandler
|
||||
import net.woggioni.rbcs.server.handler.ReadTriggerDuplexHandler
|
||||
import net.woggioni.rbcs.server.handler.ServerHandler
|
||||
import net.woggioni.rbcs.server.throttling.BucketManager
|
||||
import net.woggioni.rbcs.server.throttling.ThrottlingHandler
|
||||
|
||||
class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
|
||||
@@ -87,7 +86,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user")
|
||||
val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group")
|
||||
|
||||
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/rbcs/server/rbcs-default.xml".toUrl() }
|
||||
val DEFAULT_CONFIGURATION_URL by lazy { "jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/rbcs-default.xml".toUrl() }
|
||||
private const val SSL_HANDLER_NAME = "sslHandler"
|
||||
|
||||
fun loadConfiguration(configurationFile: Path): Configuration {
|
||||
@@ -209,7 +208,6 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
private val cfg: Configuration,
|
||||
private val channelFactory : ChannelFactory<SocketChannel>,
|
||||
private val datagramChannelFactory : ChannelFactory<DatagramChannel>,
|
||||
private val eventExecutorGroup: EventExecutorGroup
|
||||
) : ChannelInitializer<Channel>(), AsyncCloseable {
|
||||
|
||||
companion object {
|
||||
@@ -230,7 +228,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
val clientAuth = tls.trustStore?.let { trustStore ->
|
||||
val ts = loadKeystore(trustStore.file, trustStore.password)
|
||||
trustManager(
|
||||
ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus)
|
||||
getTrustManager(ts, trustStore.isCheckCertificateStatus)
|
||||
)
|
||||
if (trustStore.isRequireClientCertificate) ClientAuth.REQUIRE
|
||||
else ClientAuth.OPTIONAL
|
||||
@@ -240,27 +238,6 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadKeystore(file: Path, password: String?): KeyStore {
|
||||
val ext = JWO.splitExtension(file)
|
||||
.map(Tuple2<String, String>::get_2)
|
||||
.orElseThrow {
|
||||
IllegalArgumentException(
|
||||
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
|
||||
)
|
||||
}
|
||||
val keystore = when (ext.substring(1).lowercase()) {
|
||||
"jks" -> KeyStore.getInstance("JKS")
|
||||
"p12", "pfx" -> KeyStore.getInstance("PKCS12")
|
||||
else -> throw IllegalArgumentException(
|
||||
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
|
||||
)
|
||||
}
|
||||
Files.newInputStream(file).use {
|
||||
keystore.load(it, password?.let(String::toCharArray))
|
||||
}
|
||||
return keystore
|
||||
}
|
||||
|
||||
private val log = createLogger<ServerInitializer>()
|
||||
}
|
||||
|
||||
@@ -321,6 +298,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
"Closed connection ${ch.id().asShortText()} with ${ch.remoteAddress()}"
|
||||
}
|
||||
}
|
||||
ch.config().isAutoRead = false
|
||||
val pipeline = ch.pipeline()
|
||||
cfg.connection.also { conn ->
|
||||
val readIdleTimeout = conn.readIdleTimeout.toMillis()
|
||||
@@ -363,24 +341,27 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
sslContext?.newHandler(ch.alloc())?.also {
|
||||
pipeline.addLast(SSL_HANDLER_NAME, it)
|
||||
}
|
||||
pipeline.addLast(HttpServerCodec())
|
||||
val httpDecoderConfig = HttpDecoderConfig().apply {
|
||||
maxChunkSize = cfg.connection.chunkSize
|
||||
}
|
||||
pipeline.addLast(HttpServerCodec(httpDecoderConfig))
|
||||
pipeline.addLast(ReadTriggerDuplexHandler.NAME, ReadTriggerDuplexHandler)
|
||||
pipeline.addLast(MaxRequestSizeHandler.NAME, MaxRequestSizeHandler(cfg.connection.maxRequestSize))
|
||||
pipeline.addLast(HttpChunkContentCompressor(1024))
|
||||
pipeline.addLast(ChunkedWriteHandler())
|
||||
authenticator?.let {
|
||||
pipeline.addLast(it)
|
||||
}
|
||||
pipeline.addLast(ThrottlingHandler(bucketManager, cfg.connection))
|
||||
pipeline.addLast(ThrottlingHandler(bucketManager,cfg.rateLimiter, cfg.connection))
|
||||
|
||||
val serverHandler = let {
|
||||
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
|
||||
ServerHandler(prefix)
|
||||
ServerHandler(prefix) {
|
||||
cacheHandlerFactory.newHandler(cfg, ch.eventLoop(), channelFactory, datagramChannelFactory)
|
||||
}
|
||||
}
|
||||
pipeline.addLast(eventExecutorGroup, ServerHandler.NAME, serverHandler)
|
||||
|
||||
pipeline.addLast(cacheHandlerFactory.newHandler(ch.eventLoop(), channelFactory, datagramChannelFactory))
|
||||
pipeline.addLast(TraceHandler)
|
||||
pipeline.addLast(ExceptionHandler)
|
||||
pipeline.addLast(ServerHandler.NAME, serverHandler)
|
||||
pipeline.addLast(ExceptionHandler.NAME, ExceptionHandler)
|
||||
}
|
||||
|
||||
override fun asyncClose() = cacheHandlerFactory.asyncClose()
|
||||
@@ -391,13 +372,14 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
private val bossGroup: EventExecutorGroup,
|
||||
private val executorGroups: Iterable<EventExecutorGroup>,
|
||||
private val serverInitializer: AsyncCloseable,
|
||||
) : Future<Void> by from(closeFuture, executorGroups, serverInitializer) {
|
||||
) : Future<Void> by from(closeFuture, bossGroup, executorGroups, serverInitializer) {
|
||||
|
||||
companion object {
|
||||
private val log = createLogger<ServerHandle>()
|
||||
|
||||
private fun from(
|
||||
closeFuture: ChannelFuture,
|
||||
bossGroup: EventExecutorGroup,
|
||||
executorGroups: Iterable<EventExecutorGroup>,
|
||||
serverInitializer: AsyncCloseable
|
||||
): CompletableFuture<Void> {
|
||||
@@ -405,22 +387,15 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
closeFuture.addListener {
|
||||
val errors = mutableListOf<Throwable>()
|
||||
val deadline = Instant.now().plusSeconds(20)
|
||||
try {
|
||||
serverInitializer.close()
|
||||
} catch (ex: Throwable) {
|
||||
log.error(ex.message, ex)
|
||||
errors.addLast(ex)
|
||||
}
|
||||
|
||||
serverInitializer.asyncClose().whenComplete { _, ex ->
|
||||
serverInitializer.asyncClose().whenCompleteAsync { _, ex ->
|
||||
if(ex != null) {
|
||||
log.error(ex.message, ex)
|
||||
errors.addLast(ex)
|
||||
}
|
||||
|
||||
executorGroups.map {
|
||||
it.shutdownGracefully()
|
||||
}
|
||||
executorGroups.forEach(EventExecutorGroup::shutdownGracefully)
|
||||
bossGroup.terminationFuture().sync()
|
||||
|
||||
for (executorGroup in executorGroups) {
|
||||
val future = executorGroup.terminationFuture()
|
||||
@@ -465,20 +440,13 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
|
||||
fun run(): ServerHandle {
|
||||
// Create the multithreaded event loops for the server
|
||||
val bossGroup = NioEventLoopGroup(1)
|
||||
val bossGroup = MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory())
|
||||
val channelFactory = ChannelFactory<SocketChannel> { NioSocketChannel() }
|
||||
val datagramChannelFactory = ChannelFactory<DatagramChannel> { NioDatagramChannel() }
|
||||
val serverChannelFactory = ChannelFactory<ServerSocketChannel> { NioServerSocketChannel() }
|
||||
val workerGroup = NioEventLoopGroup(0)
|
||||
val eventExecutorGroup = run {
|
||||
val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) {
|
||||
Thread.ofVirtual().factory()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory)
|
||||
}
|
||||
val serverInitializer = ServerInitializer(cfg, channelFactory, datagramChannelFactory, workerGroup)
|
||||
val workerGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory())
|
||||
|
||||
val serverInitializer = ServerInitializer(cfg, channelFactory, datagramChannelFactory)
|
||||
val bootstrap = ServerBootstrap().apply {
|
||||
// Configure the server
|
||||
group(bossGroup, workerGroup)
|
||||
@@ -499,7 +467,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||
return ServerHandle(
|
||||
httpChannel.closeFuture(),
|
||||
bossGroup,
|
||||
setOf(workerGroup, eventExecutorGroup),
|
||||
setOf(workerGroup),
|
||||
serverInitializer
|
||||
)
|
||||
}
|
||||
|
@@ -1,90 +0,0 @@
|
||||
package net.woggioni.rbcs.server.auth
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
import io.netty.handler.ssl.SslHandler
|
||||
import io.netty.handler.ssl.SslHandshakeCompletionEvent
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.PKIXParameters
|
||||
import java.security.cert.PKIXRevocationChecker
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.EnumSet
|
||||
import javax.net.ssl.SSLSession
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
|
||||
class ClientCertificateValidator private constructor(
|
||||
private val sslHandler: SslHandler,
|
||||
private val x509TrustManager: X509TrustManager
|
||||
) : ChannelInboundHandlerAdapter() {
|
||||
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||
if (evt is SslHandshakeCompletionEvent) {
|
||||
if (evt.isSuccess) {
|
||||
val session: SSLSession = sslHandler.engine().session
|
||||
val clientCertificateChain = session.peerCertificates as Array<X509Certificate>
|
||||
val authType: String = clientCertificateChain[0].publicKey.algorithm
|
||||
x509TrustManager.checkClientTrusted(clientCertificateChain, authType)
|
||||
} else {
|
||||
// Handle the failure, for example by closing the channel.
|
||||
}
|
||||
}
|
||||
super.userEventTriggered(ctx, evt)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getTrustManager(trustStore: KeyStore?, certificateRevocationEnabled: Boolean): X509TrustManager {
|
||||
return if (trustStore != null) {
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val validator = CertPathValidator.getInstance("PKIX").apply {
|
||||
val rc = revocationChecker as PKIXRevocationChecker
|
||||
rc.options = EnumSet.of(
|
||||
PKIXRevocationChecker.Option.NO_FALLBACK
|
||||
)
|
||||
}
|
||||
val params = PKIXParameters(trustStore).apply {
|
||||
isRevocationEnabled = certificateRevocationEnabled
|
||||
}
|
||||
object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
|
||||
val clientCertificateChain = certificateFactory.generateCertPath(chain.toList())
|
||||
try {
|
||||
validator.validate(clientCertificateChain, params)
|
||||
} catch (ex: CertPathValidatorException) {
|
||||
throw CertificateException(ex)
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
private val acceptedIssuers = trustStore.aliases().asSequence()
|
||||
.filter(trustStore::isCertificateEntry)
|
||||
.map(trustStore::getCertificate)
|
||||
.map { it as X509Certificate }
|
||||
.toList()
|
||||
.toTypedArray()
|
||||
|
||||
override fun getAcceptedIssuers() = acceptedIssuers
|
||||
}
|
||||
} else {
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }
|
||||
.single() as X509TrustManager
|
||||
}
|
||||
}
|
||||
|
||||
fun of(
|
||||
sslHandler: SslHandler,
|
||||
trustStore: KeyStore?,
|
||||
certificateRevocationEnabled: Boolean
|
||||
): ClientCertificateValidator {
|
||||
return ClientCertificateValidator(sslHandler, getTrustManager(trustStore, certificateRevocationEnabled))
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,8 +8,9 @@ class RoleAuthorizer : Authorizer {
|
||||
|
||||
companion object {
|
||||
private val METHOD_MAP = mapOf(
|
||||
Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE),
|
||||
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST)
|
||||
Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD),
|
||||
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST),
|
||||
Role.Healthcheck to setOf(HttpMethod.TRACE)
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,5 @@
|
||||
package net.woggioni.rbcs.server.cache
|
||||
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.rbcs.api.AsyncCloseable
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.ObjectInputStream
|
||||
@@ -20,6 +16,10 @@ import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.rbcs.api.AsyncCloseable
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
|
||||
class FileSystemCache(
|
||||
val root: Path,
|
||||
|
@@ -4,12 +4,12 @@ import io.netty.channel.ChannelFactory
|
||||
import io.netty.channel.EventLoopGroup
|
||||
import io.netty.channel.socket.DatagramChannel
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import net.woggioni.jwo.Application
|
||||
import net.woggioni.rbcs.api.CacheHandlerFactory
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.RBCS
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
|
||||
data class FileSystemCacheConfiguration(
|
||||
val root: Path?,
|
||||
@@ -17,7 +17,6 @@ data class FileSystemCacheConfiguration(
|
||||
val digestAlgorithm : String?,
|
||||
val compressionEnabled: Boolean,
|
||||
val compressionLevel: Int,
|
||||
val chunkSize: Int,
|
||||
) : Configuration.Cache {
|
||||
|
||||
override fun materialize() = object : CacheHandlerFactory {
|
||||
@@ -26,10 +25,11 @@ data class FileSystemCacheConfiguration(
|
||||
override fun asyncClose() = cache.asyncClose()
|
||||
|
||||
override fun newHandler(
|
||||
cfg : Configuration,
|
||||
eventLoop: EventLoopGroup,
|
||||
socketChannelFactory: ChannelFactory<SocketChannel>,
|
||||
datagramChannelFactory: ChannelFactory<DatagramChannel>
|
||||
) = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, chunkSize)
|
||||
) = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, cfg.connection.chunkSize)
|
||||
}
|
||||
|
||||
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI
|
||||
|
@@ -2,9 +2,14 @@ package net.woggioni.rbcs.server.cache
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.handler.codec.http.LastHttpContent
|
||||
import io.netty.handler.stream.ChunkedNioFile
|
||||
import java.nio.channels.Channels
|
||||
import java.util.Base64
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
import net.woggioni.rbcs.api.CacheHandler
|
||||
import net.woggioni.rbcs.api.message.CacheMessage
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest
|
||||
@@ -14,11 +19,6 @@ import net.woggioni.rbcs.api.message.CacheMessage.CacheValueFoundResponse
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheValueNotFoundResponse
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
|
||||
import net.woggioni.rbcs.common.RBCS.processCacheKey
|
||||
import java.nio.channels.Channels
|
||||
import java.util.Base64
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
class FileSystemCacheHandler(
|
||||
private val cache: FileSystemCache,
|
||||
@@ -26,12 +26,18 @@ class FileSystemCacheHandler(
|
||||
private val compressionEnabled: Boolean,
|
||||
private val compressionLevel: Int,
|
||||
private val chunkSize: Int
|
||||
) : SimpleChannelInboundHandler<CacheMessage>() {
|
||||
) : CacheHandler() {
|
||||
|
||||
private interface InProgressRequest{
|
||||
|
||||
}
|
||||
|
||||
private class InProgressGetRequest(val request : CacheGetRequest) : InProgressRequest
|
||||
|
||||
private inner class InProgressPutRequest(
|
||||
val key : String,
|
||||
private val fileSink : FileSystemCache.FileSink
|
||||
) {
|
||||
) : InProgressRequest {
|
||||
|
||||
private val stream = Channels.newOutputStream(fileSink.channel).let {
|
||||
if (compressionEnabled) {
|
||||
@@ -55,7 +61,7 @@ class FileSystemCacheHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private var inProgressPutRequest: InProgressPutRequest? = null
|
||||
private var inProgressRequest: InProgressRequest? = null
|
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
|
||||
when (msg) {
|
||||
@@ -68,55 +74,64 @@ class FileSystemCacheHandler(
|
||||
}
|
||||
|
||||
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
|
||||
val key = String(Base64.getUrlEncoder().encode(processCacheKey(msg.key, digestAlgorithm)))
|
||||
cache.get(key)?.also { entryValue ->
|
||||
ctx.writeAndFlush(CacheValueFoundResponse(msg.key, entryValue.metadata))
|
||||
entryValue.channel.let { channel ->
|
||||
if(compressionEnabled) {
|
||||
InflaterInputStream(Channels.newInputStream(channel)).use { stream ->
|
||||
inProgressRequest = InProgressGetRequest(msg)
|
||||
|
||||
outerLoop@
|
||||
while (true) {
|
||||
val buf = ctx.alloc().heapBuffer(chunkSize)
|
||||
while(buf.readableBytes() < chunkSize) {
|
||||
val read = buf.writeBytes(stream, chunkSize)
|
||||
if(read < 0) {
|
||||
ctx.writeAndFlush(LastCacheContent(buf))
|
||||
break@outerLoop
|
||||
}
|
||||
}
|
||||
ctx.writeAndFlush(CacheContent(buf))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.writeAndFlush(ChunkedNioFile(channel, entryValue.offset, entryValue.size - entryValue.offset, chunkSize))
|
||||
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
|
||||
}
|
||||
}
|
||||
} ?: ctx.writeAndFlush(CacheValueNotFoundResponse())
|
||||
}
|
||||
|
||||
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
|
||||
val key = String(Base64.getUrlEncoder().encode(processCacheKey(msg.key, digestAlgorithm)))
|
||||
val key = String(Base64.getUrlEncoder().encode(processCacheKey(msg.key, null, digestAlgorithm)))
|
||||
val sink = cache.put(key, msg.metadata)
|
||||
inProgressPutRequest = InProgressPutRequest(msg.key, sink)
|
||||
inProgressRequest = InProgressPutRequest(msg.key, sink)
|
||||
}
|
||||
|
||||
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
|
||||
inProgressPutRequest!!.write(msg.content())
|
||||
val request = inProgressRequest
|
||||
if(request is InProgressPutRequest) {
|
||||
request.write(msg.content())
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
|
||||
inProgressPutRequest?.let { request ->
|
||||
inProgressPutRequest = null
|
||||
request.write(msg.content())
|
||||
request.commit()
|
||||
ctx.writeAndFlush(CachePutResponse(request.key))
|
||||
when(val request = inProgressRequest) {
|
||||
is InProgressPutRequest -> {
|
||||
inProgressRequest = null
|
||||
request.write(msg.content())
|
||||
request.commit()
|
||||
sendMessageAndFlush(ctx, CachePutResponse(request.key))
|
||||
}
|
||||
is InProgressGetRequest -> {
|
||||
val key = String(Base64.getUrlEncoder().encode(processCacheKey(request.request.key, null, digestAlgorithm)))
|
||||
cache.get(key)?.also { entryValue ->
|
||||
sendMessageAndFlush(ctx, CacheValueFoundResponse(request.request.key, entryValue.metadata))
|
||||
entryValue.channel.let { channel ->
|
||||
if(compressionEnabled) {
|
||||
InflaterInputStream(Channels.newInputStream(channel)).use { stream ->
|
||||
|
||||
outerLoop@
|
||||
while (true) {
|
||||
val buf = ctx.alloc().heapBuffer(chunkSize)
|
||||
while(buf.readableBytes() < chunkSize) {
|
||||
val read = buf.writeBytes(stream, chunkSize)
|
||||
if(read < 0) {
|
||||
sendMessageAndFlush(ctx, LastCacheContent(buf))
|
||||
break@outerLoop
|
||||
}
|
||||
}
|
||||
sendMessageAndFlush(ctx, CacheContent(buf))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendMessage(ctx, ChunkedNioFile(channel, entryValue.offset, entryValue.size - entryValue.offset, chunkSize))
|
||||
sendMessageAndFlush(ctx, LastHttpContent.EMPTY_LAST_CONTENT)
|
||||
}
|
||||
}
|
||||
} ?: sendMessageAndFlush(ctx, CacheValueNotFoundResponse(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
inProgressPutRequest?.rollback()
|
||||
(inProgressRequest as? InProgressPutRequest)?.rollback()
|
||||
super.exceptionCaught(ctx, cause)
|
||||
}
|
||||
}
|
@@ -1,18 +1,18 @@
|
||||
package net.woggioni.rbcs.server.cache
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.zip.Deflater
|
||||
import net.woggioni.rbcs.api.CacheProvider
|
||||
import net.woggioni.rbcs.common.RBCS
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.zip.Deflater
|
||||
|
||||
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
||||
|
||||
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd"
|
||||
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs-server.xsd"
|
||||
|
||||
override fun getXmlType() = "fileSystemCacheType"
|
||||
|
||||
@@ -31,9 +31,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
||||
?.let(String::toInt)
|
||||
?: Deflater.DEFAULT_COMPRESSION
|
||||
val digestAlgorithm = el.renderAttribute("digest")
|
||||
val chunkSize = el.renderAttribute("chunk-size")
|
||||
?.let(Integer::decode)
|
||||
?: 0x10000
|
||||
|
||||
return FileSystemCacheConfiguration(
|
||||
path,
|
||||
@@ -41,7 +38,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
||||
digestAlgorithm,
|
||||
enableCompression,
|
||||
compressionLevel,
|
||||
chunkSize
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,7 +59,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
||||
}?.let {
|
||||
attr("compression-level", it.toString())
|
||||
}
|
||||
attr("chunk-size", chunkSize.toString())
|
||||
}
|
||||
result
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
package net.woggioni.rbcs.server.cache
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.PriorityQueue
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.withLock
|
||||
import net.woggioni.rbcs.api.AsyncCloseable
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.PriorityBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
private class CacheKey(private val value: ByteArray) {
|
||||
override fun equals(other: Any?) = if (other is CacheKey) {
|
||||
@@ -34,15 +34,17 @@ class InMemoryCache(
|
||||
private val log = createLogger<InMemoryCache>()
|
||||
}
|
||||
|
||||
private val size = AtomicLong()
|
||||
private val map = ConcurrentHashMap<CacheKey, CacheEntry>()
|
||||
private var mapSize : Long = 0
|
||||
private val map = HashMap<CacheKey, CacheEntry>()
|
||||
private val lock = ReentrantReadWriteLock()
|
||||
private val cond = lock.writeLock().newCondition()
|
||||
|
||||
private class RemovalQueueElement(val key: CacheKey, val value: CacheEntry, val expiry: Instant) :
|
||||
Comparable<RemovalQueueElement> {
|
||||
override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry)
|
||||
}
|
||||
|
||||
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>()
|
||||
private val removalQueue = PriorityQueue<RemovalQueueElement>()
|
||||
|
||||
@Volatile
|
||||
private var running = true
|
||||
@@ -51,21 +53,32 @@ class InMemoryCache(
|
||||
init {
|
||||
Thread.ofVirtual().name("in-memory-cache-gc").start {
|
||||
try {
|
||||
while (running) {
|
||||
val el = removalQueue.poll(1, TimeUnit.SECONDS) ?: continue
|
||||
val value = el.value
|
||||
val now = Instant.now()
|
||||
if (now > el.expiry) {
|
||||
val removed = map.remove(el.key, value)
|
||||
if (removed) {
|
||||
updateSizeAfterRemoval(value.content)
|
||||
//Decrease the reference count for map
|
||||
value.content.release()
|
||||
lock.writeLock().withLock {
|
||||
while (running) {
|
||||
val el = removalQueue.poll()
|
||||
if(el == null) {
|
||||
cond.await(1000, TimeUnit.MILLISECONDS)
|
||||
continue
|
||||
}
|
||||
val value = el.value
|
||||
val now = Instant.now()
|
||||
if (now > el.expiry) {
|
||||
val removed = map.remove(el.key, value)
|
||||
if (removed) {
|
||||
updateSizeAfterRemoval(value.content)
|
||||
//Decrease the reference count for map
|
||||
value.content.release()
|
||||
}
|
||||
} else {
|
||||
removalQueue.offer(el)
|
||||
val interval = minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1))
|
||||
cond.await(interval.toMillis(), TimeUnit.MILLISECONDS)
|
||||
}
|
||||
} else {
|
||||
removalQueue.put(el)
|
||||
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
|
||||
}
|
||||
map.forEach {
|
||||
it.value.content.release()
|
||||
}
|
||||
map.clear()
|
||||
}
|
||||
complete(null)
|
||||
} catch (ex: Throwable) {
|
||||
@@ -77,7 +90,7 @@ class InMemoryCache(
|
||||
|
||||
fun removeEldest(): Long {
|
||||
while (true) {
|
||||
val el = removalQueue.take()
|
||||
val el = removalQueue.poll() ?: return mapSize
|
||||
val value = el.value
|
||||
val removed = map.remove(el.key, value)
|
||||
if (removed) {
|
||||
@@ -90,18 +103,22 @@ class InMemoryCache(
|
||||
}
|
||||
|
||||
private fun updateSizeAfterRemoval(removed: ByteBuf): Long {
|
||||
return size.updateAndGet { currentSize: Long ->
|
||||
currentSize - removed.readableBytes()
|
||||
}
|
||||
mapSize -= removed.readableBytes()
|
||||
return mapSize
|
||||
}
|
||||
|
||||
override fun asyncClose() : CompletableFuture<Void> {
|
||||
running = false
|
||||
lock.writeLock().withLock {
|
||||
cond.signal()
|
||||
}
|
||||
return closeFuture
|
||||
}
|
||||
|
||||
fun get(key: ByteArray) = map[CacheKey(key)]?.run {
|
||||
CacheEntry(metadata, content.retainedDuplicate())
|
||||
fun get(key: ByteArray) = lock.readLock().withLock {
|
||||
map[CacheKey(key)]?.run {
|
||||
CacheEntry(metadata, content.retainedDuplicate())
|
||||
}
|
||||
}
|
||||
|
||||
fun put(
|
||||
@@ -109,18 +126,18 @@ class InMemoryCache(
|
||||
value: CacheEntry,
|
||||
) {
|
||||
val cacheKey = CacheKey(key)
|
||||
val oldSize = map.put(cacheKey, value)?.let { old ->
|
||||
val result = old.content.readableBytes()
|
||||
old.content.release()
|
||||
result
|
||||
} ?: 0
|
||||
val delta = value.content.readableBytes() - oldSize
|
||||
var newSize = size.updateAndGet { currentSize: Long ->
|
||||
currentSize + delta
|
||||
}
|
||||
removalQueue.put(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
|
||||
while (newSize > maxSize) {
|
||||
newSize = removeEldest()
|
||||
lock.writeLock().withLock {
|
||||
val oldSize = map.put(cacheKey, value)?.let { old ->
|
||||
val result = old.content.readableBytes()
|
||||
old.content.release()
|
||||
result
|
||||
} ?: 0
|
||||
val delta = value.content.readableBytes() - oldSize
|
||||
mapSize += delta
|
||||
removalQueue.offer(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
|
||||
while (mapSize > maxSize) {
|
||||
removeEldest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,11 +4,10 @@ import io.netty.channel.ChannelFactory
|
||||
import io.netty.channel.EventLoopGroup
|
||||
import io.netty.channel.socket.DatagramChannel
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.util.concurrent.Future
|
||||
import java.time.Duration
|
||||
import net.woggioni.rbcs.api.CacheHandlerFactory
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.RBCS
|
||||
import java.time.Duration
|
||||
|
||||
data class InMemoryCacheConfiguration(
|
||||
val maxAge: Duration,
|
||||
@@ -16,7 +15,6 @@ data class InMemoryCacheConfiguration(
|
||||
val digestAlgorithm : String?,
|
||||
val compressionEnabled: Boolean,
|
||||
val compressionLevel: Int,
|
||||
val chunkSize : Int
|
||||
) : Configuration.Cache {
|
||||
override fun materialize() = object : CacheHandlerFactory {
|
||||
private val cache = InMemoryCache(maxAge, maxSize)
|
||||
@@ -24,6 +22,7 @@ data class InMemoryCacheConfiguration(
|
||||
override fun asyncClose() = cache.asyncClose()
|
||||
|
||||
override fun newHandler(
|
||||
cfg : Configuration,
|
||||
eventLoop: EventLoopGroup,
|
||||
socketChannelFactory: ChannelFactory<SocketChannel>,
|
||||
datagramChannelFactory: ChannelFactory<DatagramChannel>
|
||||
|
@@ -2,7 +2,10 @@ package net.woggioni.rbcs.server.cache
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterOutputStream
|
||||
import net.woggioni.rbcs.api.CacheHandler
|
||||
import net.woggioni.rbcs.api.message.CacheMessage
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest
|
||||
@@ -13,18 +16,23 @@ import net.woggioni.rbcs.api.message.CacheMessage.CacheValueNotFoundResponse
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
|
||||
import net.woggioni.rbcs.common.ByteBufOutputStream
|
||||
import net.woggioni.rbcs.common.RBCS.processCacheKey
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterOutputStream
|
||||
|
||||
class InMemoryCacheHandler(
|
||||
private val cache: InMemoryCache,
|
||||
private val digestAlgorithm: String?,
|
||||
private val compressionEnabled: Boolean,
|
||||
private val compressionLevel: Int
|
||||
) : SimpleChannelInboundHandler<CacheMessage>() {
|
||||
) : CacheHandler() {
|
||||
|
||||
private interface InProgressPutRequest : AutoCloseable {
|
||||
private interface InProgressRequest : AutoCloseable {
|
||||
}
|
||||
|
||||
private class InProgressGetRequest(val request: CacheGetRequest) : InProgressRequest {
|
||||
override fun close() {
|
||||
}
|
||||
}
|
||||
|
||||
private interface InProgressPutRequest : InProgressRequest {
|
||||
val request: CachePutRequest
|
||||
val buf: ByteBuf
|
||||
|
||||
@@ -33,18 +41,14 @@ class InMemoryCacheHandler(
|
||||
|
||||
private inner class InProgressPlainPutRequest(ctx: ChannelHandlerContext, override val request: CachePutRequest) :
|
||||
InProgressPutRequest {
|
||||
override val buf = ctx.alloc().compositeBuffer()
|
||||
|
||||
private val stream = ByteBufOutputStream(buf).let {
|
||||
if (compressionEnabled) {
|
||||
DeflaterOutputStream(it, Deflater(compressionLevel))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
override val buf = ctx.alloc().compositeHeapBuffer()
|
||||
|
||||
override fun append(buf: ByteBuf) {
|
||||
this.buf.addComponent(true, buf.retain())
|
||||
if (buf.isDirect) {
|
||||
this.buf.writeBytes(buf)
|
||||
} else {
|
||||
this.buf.addComponent(true, buf.retain())
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
@@ -72,7 +76,7 @@ class InMemoryCacheHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private var inProgressPutRequest: InProgressPutRequest? = null
|
||||
private var inProgressRequest: InProgressRequest? = null
|
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
|
||||
when (msg) {
|
||||
@@ -85,24 +89,11 @@ class InMemoryCacheHandler(
|
||||
}
|
||||
|
||||
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
|
||||
cache.get(processCacheKey(msg.key, digestAlgorithm))?.let { value ->
|
||||
ctx.writeAndFlush(CacheValueFoundResponse(msg.key, value.metadata))
|
||||
if (compressionEnabled) {
|
||||
val buf = ctx.alloc().heapBuffer()
|
||||
InflaterOutputStream(ByteBufOutputStream(buf)).use {
|
||||
value.content.readBytes(it, value.content.readableBytes())
|
||||
value.content.release()
|
||||
buf.retain()
|
||||
}
|
||||
ctx.writeAndFlush(LastCacheContent(buf))
|
||||
} else {
|
||||
ctx.writeAndFlush(LastCacheContent(value.content))
|
||||
}
|
||||
} ?: ctx.writeAndFlush(CacheValueNotFoundResponse())
|
||||
inProgressRequest = InProgressGetRequest(msg)
|
||||
}
|
||||
|
||||
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
|
||||
inProgressPutRequest = if(compressionEnabled) {
|
||||
inProgressRequest = if (compressionEnabled) {
|
||||
InProgressCompressedPutRequest(ctx, msg)
|
||||
} else {
|
||||
InProgressPlainPutRequest(ctx, msg)
|
||||
@@ -110,27 +101,47 @@ class InMemoryCacheHandler(
|
||||
}
|
||||
|
||||
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
|
||||
inProgressPutRequest?.append(msg.content())
|
||||
val req = inProgressRequest
|
||||
if (req is InProgressPutRequest) {
|
||||
req.append(msg.content())
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
|
||||
handleCacheContent(ctx, msg)
|
||||
inProgressPutRequest?.let { inProgressRequest ->
|
||||
inProgressPutRequest = null
|
||||
val buf = inProgressRequest.buf
|
||||
buf.retain()
|
||||
inProgressRequest.close()
|
||||
val cacheKey = processCacheKey(inProgressRequest.request.key, digestAlgorithm)
|
||||
cache.put(cacheKey, CacheEntry(inProgressRequest.request.metadata, buf))
|
||||
ctx.writeAndFlush(CachePutResponse(inProgressRequest.request.key))
|
||||
when (val req = inProgressRequest) {
|
||||
is InProgressGetRequest -> {
|
||||
cache.get(processCacheKey(req.request.key, null, digestAlgorithm))?.let { value ->
|
||||
sendMessageAndFlush(ctx, CacheValueFoundResponse(req.request.key, value.metadata))
|
||||
if (compressionEnabled) {
|
||||
val buf = ctx.alloc().heapBuffer()
|
||||
InflaterOutputStream(ByteBufOutputStream(buf)).use {
|
||||
value.content.readBytes(it, value.content.readableBytes())
|
||||
value.content.release()
|
||||
buf.retain()
|
||||
}
|
||||
sendMessage(ctx, LastCacheContent(buf))
|
||||
} else {
|
||||
sendMessage(ctx, LastCacheContent(value.content))
|
||||
}
|
||||
} ?: sendMessage(ctx, CacheValueNotFoundResponse(req.request.key))
|
||||
}
|
||||
|
||||
is InProgressPutRequest -> {
|
||||
this.inProgressRequest = null
|
||||
val buf = req.buf
|
||||
buf.retain()
|
||||
req.close()
|
||||
val cacheKey = processCacheKey(req.request.key, null, digestAlgorithm)
|
||||
cache.put(cacheKey, CacheEntry(req.request.metadata, buf))
|
||||
sendMessageAndFlush(ctx, CachePutResponse(req.request.key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
inProgressPutRequest?.let { req ->
|
||||
req.buf.release()
|
||||
inProgressPutRequest = null
|
||||
}
|
||||
inProgressRequest?.close()
|
||||
inProgressRequest = null
|
||||
super.exceptionCaught(ctx, cause)
|
||||
}
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
package net.woggioni.rbcs.server.cache
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.zip.Deflater
|
||||
import net.woggioni.rbcs.api.CacheProvider
|
||||
import net.woggioni.rbcs.common.RBCS
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.time.Duration
|
||||
import java.util.zip.Deflater
|
||||
|
||||
class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
||||
|
||||
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd"
|
||||
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs-server.xsd"
|
||||
|
||||
override fun getXmlType() = "inMemoryCacheType"
|
||||
|
||||
@@ -31,16 +31,12 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
||||
?.let(String::toInt)
|
||||
?: Deflater.DEFAULT_COMPRESSION
|
||||
val digestAlgorithm = el.renderAttribute("digest")
|
||||
val chunkSize = el.renderAttribute("chunk-size")
|
||||
?.let(Integer::decode)
|
||||
?: 0x10000
|
||||
return InMemoryCacheConfiguration(
|
||||
maxAge,
|
||||
maxSize,
|
||||
digestAlgorithm,
|
||||
enableCompression,
|
||||
compressionLevel,
|
||||
chunkSize
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +56,6 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
||||
}?.let {
|
||||
attr("compression-level", it.toString())
|
||||
}
|
||||
attr("chunk-size", chunkSize.toString())
|
||||
}
|
||||
result
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package net.woggioni.rbcs.server.configuration
|
||||
|
||||
import java.util.ServiceLoader
|
||||
import net.woggioni.rbcs.api.CacheProvider
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import java.util.ServiceLoader
|
||||
|
||||
object CacheSerializers {
|
||||
val index = (Configuration::class.java.module.layer?.let { layer ->
|
||||
|
@@ -1,5 +1,8 @@
|
||||
package net.woggioni.rbcs.server.configuration
|
||||
|
||||
import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.Configuration.Authentication
|
||||
import net.woggioni.rbcs.api.Configuration.BasicAuthentication
|
||||
@@ -18,20 +21,19 @@ import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.TypeInfo
|
||||
import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
object Parser {
|
||||
fun parse(document: Document): Configuration {
|
||||
val root = document.documentElement
|
||||
val anonymousUser = User("", null, emptySet(), null)
|
||||
var connection: Configuration.Connection = Configuration.Connection(
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
Duration.of(60, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
67108864
|
||||
Duration.of(60, ChronoUnit.SECONDS),
|
||||
0x4000000,
|
||||
0x10000
|
||||
)
|
||||
var rateLimiter = Configuration.RateLimiter(false, 0x100000, 100)
|
||||
var eventExecutor: Configuration.EventExecutor = Configuration.EventExecutor(true)
|
||||
var cache: Cache? = null
|
||||
var host = "127.0.0.1"
|
||||
@@ -119,20 +121,36 @@ object Parser {
|
||||
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
||||
val maxRequestSize = child.renderAttribute("max-request-size")
|
||||
?.let(Integer::decode) ?: 0x4000000
|
||||
val chunkSize = child.renderAttribute("chunk-size")
|
||||
?.let(Integer::decode) ?: 0x10000
|
||||
connection = Configuration.Connection(
|
||||
idleTimeout,
|
||||
readIdleTimeout,
|
||||
writeIdleTimeout,
|
||||
maxRequestSize
|
||||
maxRequestSize,
|
||||
chunkSize
|
||||
)
|
||||
}
|
||||
|
||||
"event-executor" -> {
|
||||
val useVirtualThread = root.renderAttribute("use-virtual-threads")
|
||||
val useVirtualThread = child.renderAttribute("use-virtual-threads")
|
||||
?.let(String::toBoolean) ?: true
|
||||
eventExecutor = Configuration.EventExecutor(useVirtualThread)
|
||||
}
|
||||
|
||||
"rate-limiter" -> {
|
||||
val delayResponse = child.renderAttribute("delay-response")
|
||||
?.let(String::toBoolean)
|
||||
?: false
|
||||
val messageBufferSize = child.renderAttribute("message-buffer-size")
|
||||
?.let(Integer::decode)
|
||||
?: 0x100000
|
||||
val maxQueuedMessages = child.renderAttribute("max-queued-messages")
|
||||
?.let(Integer::decode)
|
||||
?: 100
|
||||
rateLimiter = Configuration.RateLimiter(delayResponse, messageBufferSize, maxQueuedMessages)
|
||||
}
|
||||
|
||||
"tls" -> {
|
||||
var keyStore: KeyStore? = null
|
||||
var trustStore: TrustStore? = null
|
||||
@@ -180,6 +198,7 @@ object Parser {
|
||||
incomingConnectionsBacklogSize,
|
||||
serverPath,
|
||||
eventExecutor,
|
||||
rateLimiter,
|
||||
connection,
|
||||
users,
|
||||
groups,
|
||||
@@ -193,6 +212,7 @@ object Parser {
|
||||
when (it.localName) {
|
||||
"reader" -> Role.Reader
|
||||
"writer" -> Role.Writer
|
||||
"healthcheck" -> Role.Healthcheck
|
||||
else -> throw UnsupportedOperationException("Illegal node '${it.localName}'")
|
||||
}
|
||||
}.toSet()
|
||||
|
@@ -40,11 +40,17 @@ object Serializer {
|
||||
attr("read-idle-timeout", connection.readIdleTimeout.toString())
|
||||
attr("write-idle-timeout", connection.writeIdleTimeout.toString())
|
||||
attr("max-request-size", connection.maxRequestSize.toString())
|
||||
attr("chunk-size", connection.chunkSize.toString())
|
||||
}
|
||||
}
|
||||
node("event-executor") {
|
||||
attr("use-virtual-threads", conf.eventExecutor.isUseVirtualThreads.toString())
|
||||
}
|
||||
node("rate-limiter") {
|
||||
attr("delay-response", conf.rateLimiter.isDelayRequest.toString())
|
||||
attr("max-queued-messages", conf.rateLimiter.maxQueuedMessages.toString())
|
||||
attr("message-buffer-size", conf.rateLimiter.messageBufferSize.toString())
|
||||
}
|
||||
val cache = conf.cache
|
||||
val serializer : CacheProvider<Configuration.Cache> =
|
||||
(CacheSerializers.index[cache.namespaceURI to cache.typeName] as? CacheProvider<Configuration.Cache>) ?: throw NotImplementedError()
|
||||
|
@@ -13,6 +13,10 @@ import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import io.netty.handler.codec.http.HttpVersion
|
||||
import io.netty.handler.timeout.ReadTimeoutException
|
||||
import io.netty.handler.timeout.WriteTimeoutException
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketException
|
||||
import javax.net.ssl.SSLException
|
||||
import javax.net.ssl.SSLPeerUnverifiedException
|
||||
import net.woggioni.rbcs.api.exception.CacheException
|
||||
import net.woggioni.rbcs.api.exception.ContentTooLargeException
|
||||
import net.woggioni.rbcs.common.contextLogger
|
||||
@@ -20,13 +24,12 @@ import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.common.log
|
||||
import org.slf4j.event.Level
|
||||
import org.slf4j.spi.LoggingEventBuilder
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketException
|
||||
import javax.net.ssl.SSLException
|
||||
import javax.net.ssl.SSLPeerUnverifiedException
|
||||
|
||||
@Sharable
|
||||
object ExceptionHandler : ChannelDuplexHandler() {
|
||||
|
||||
val NAME : String = this::class.java.name
|
||||
|
||||
private val log = contextLogger()
|
||||
|
||||
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package net.woggioni.rbcs.server.handler
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.handler.codec.http.HttpContent
|
||||
|
||||
class BlackHoleRequestHandler : SimpleChannelInboundHandler<HttpContent>() {
|
||||
companion object {
|
||||
val NAME = BlackHoleRequestHandler::class.java.name
|
||||
}
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpContent) {
|
||||
}
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
package net.woggioni.rbcs.server.handler
|
||||
|
||||
import io.netty.channel.ChannelHandler.Sharable
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.handler.codec.http.HttpContent
|
||||
import io.netty.handler.codec.http.LastHttpContent
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
|
||||
|
||||
@Sharable
|
||||
object CacheContentHandler : SimpleChannelInboundHandler<HttpContent>() {
|
||||
val NAME = this::class.java.name
|
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpContent) {
|
||||
when(msg) {
|
||||
is LastHttpContent -> {
|
||||
ctx.fireChannelRead(LastCacheContent(msg.content().retain()))
|
||||
ctx.pipeline().remove(this)
|
||||
}
|
||||
else -> ctx.fireChannelRead(CacheContent(msg.content().retain()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) {
|
||||
super.exceptionCaught(ctx, cause)
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package net.woggioni.rbcs.server.handler
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler
|
||||
import io.netty.channel.ChannelHandler.Sharable
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelPromise
|
||||
import io.netty.handler.codec.http.LastHttpContent
|
||||
|
||||
@Sharable
|
||||
object ReadTriggerDuplexHandler : ChannelDuplexHandler() {
|
||||
val NAME = ReadTriggerDuplexHandler::class.java.name
|
||||
|
||||
override fun handlerAdded(ctx: ChannelHandlerContext) {
|
||||
ctx.read()
|
||||
}
|
||||
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
super.channelRead(ctx, msg)
|
||||
if(msg !is LastHttpContent) {
|
||||
ctx.read()
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(
|
||||
ctx: ChannelHandlerContext,
|
||||
msg: Any,
|
||||
promise: ChannelPromise
|
||||
) {
|
||||
super.write(ctx, msg, promise)
|
||||
if(msg is LastHttpContent) {
|
||||
ctx.read()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,12 +1,14 @@
|
||||
package net.woggioni.rbcs.server.handler
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler
|
||||
import io.netty.channel.ChannelHandler
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelPromise
|
||||
import io.netty.handler.codec.http.DefaultFullHttpResponse
|
||||
import io.netty.handler.codec.http.DefaultHttpContent
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse
|
||||
import io.netty.handler.codec.http.DefaultLastHttpContent
|
||||
import io.netty.handler.codec.http.HttpContent
|
||||
import io.netty.handler.codec.http.HttpHeaderNames
|
||||
import io.netty.handler.codec.http.HttpHeaderValues
|
||||
import io.netty.handler.codec.http.HttpHeaders
|
||||
@@ -15,6 +17,8 @@ import io.netty.handler.codec.http.HttpRequest
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import io.netty.handler.codec.http.HttpUtil
|
||||
import io.netty.handler.codec.http.HttpVersion
|
||||
import io.netty.handler.codec.http.LastHttpContent
|
||||
import java.nio.file.Path
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.api.message.CacheMessage
|
||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
||||
@@ -27,15 +31,14 @@ import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.common.warn
|
||||
import java.nio.file.Path
|
||||
import java.util.Locale
|
||||
import net.woggioni.rbcs.server.exception.ExceptionHandler
|
||||
|
||||
class ServerHandler(private val serverPrefix: Path) :
|
||||
class ServerHandler(private val serverPrefix: Path, private val cacheHandlerSupplier : () -> ChannelHandler) :
|
||||
ChannelDuplexHandler() {
|
||||
|
||||
companion object {
|
||||
private val log = createLogger<ServerHandler>()
|
||||
val NAME = this::class.java.name
|
||||
val NAME = ServerHandler::class.java.name
|
||||
}
|
||||
|
||||
private var httpVersion = HttpVersion.HTTP_1_1
|
||||
@@ -59,20 +62,36 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
}
|
||||
}
|
||||
|
||||
private var cacheRequestInProgress : Boolean = false
|
||||
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
when (msg) {
|
||||
is HttpRequest -> handleRequest(ctx, msg)
|
||||
is HttpContent -> {
|
||||
if(cacheRequestInProgress) {
|
||||
if(msg is LastHttpContent) {
|
||||
super.channelRead(ctx, LastCacheContent(msg.content().retain()))
|
||||
cacheRequestInProgress = false
|
||||
} else {
|
||||
super.channelRead(ctx, CacheContent(msg.content().retain()))
|
||||
}
|
||||
msg.release()
|
||||
} else {
|
||||
super.channelRead(ctx, msg)
|
||||
}
|
||||
}
|
||||
else -> super.channelRead(ctx, msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise?) {
|
||||
if (msg is CacheMessage) {
|
||||
try {
|
||||
when (msg) {
|
||||
is CachePutResponse -> {
|
||||
log.debug(ctx) {
|
||||
"Added value for key '${msg.key}' to build cache"
|
||||
}
|
||||
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.CREATED)
|
||||
val keyBytes = msg.key.toByteArray(Charsets.UTF_8)
|
||||
response.headers().apply {
|
||||
@@ -88,6 +107,9 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
}
|
||||
|
||||
is CacheValueNotFoundResponse -> {
|
||||
log.debug(ctx) {
|
||||
"Value not found for key '${msg.key}'"
|
||||
}
|
||||
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.NOT_FOUND)
|
||||
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
|
||||
setKeepAliveHeader(response.headers())
|
||||
@@ -95,6 +117,9 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
}
|
||||
|
||||
is CacheValueFoundResponse -> {
|
||||
log.debug(ctx) {
|
||||
"Retrieved value for key '${msg.key}'"
|
||||
}
|
||||
val response = DefaultHttpResponse(httpVersion, HttpResponseStatus.OK)
|
||||
response.headers().apply {
|
||||
set(HttpHeaderNames.CONTENT_TYPE, msg.metadata.mimeType ?: HttpHeaderValues.APPLICATION_OCTET_STREAM)
|
||||
@@ -127,6 +152,8 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
} finally {
|
||||
resetRequestMetadata()
|
||||
}
|
||||
} else if(msg is LastHttpContent) {
|
||||
ctx.write(msg, promise)
|
||||
} else super.write(ctx, msg, promise)
|
||||
}
|
||||
|
||||
@@ -137,13 +164,16 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
if (method === HttpMethod.GET) {
|
||||
val path = Path.of(msg.uri()).normalize()
|
||||
if (path.startsWith(serverPrefix)) {
|
||||
cacheRequestInProgress = true
|
||||
val relativePath = serverPrefix.relativize(path)
|
||||
val key = relativePath.toString()
|
||||
ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler)
|
||||
val key : String = relativePath.toString()
|
||||
val cacheHandler = cacheHandlerSupplier()
|
||||
ctx.pipeline().addBefore(ExceptionHandler.NAME, null, cacheHandler)
|
||||
key.let(::CacheGetRequest)
|
||||
.let(ctx::fireChannelRead)
|
||||
?: ctx.channel().write(CacheValueNotFoundResponse())
|
||||
?: ctx.channel().write(CacheValueNotFoundResponse(key))
|
||||
} else {
|
||||
cacheRequestInProgress = false
|
||||
log.warn(ctx) {
|
||||
"Got request for unhandled path '${msg.uri()}'"
|
||||
}
|
||||
@@ -154,20 +184,21 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
} else if (method === HttpMethod.PUT) {
|
||||
val path = Path.of(msg.uri()).normalize()
|
||||
if (path.startsWith(serverPrefix)) {
|
||||
cacheRequestInProgress = true
|
||||
val relativePath = serverPrefix.relativize(path)
|
||||
val key = relativePath.toString()
|
||||
log.debug(ctx) {
|
||||
"Added value for key '$key' to build cache"
|
||||
}
|
||||
ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler)
|
||||
val cacheHandler = cacheHandlerSupplier()
|
||||
ctx.pipeline().addAfter(NAME, null, cacheHandler)
|
||||
|
||||
path.fileName?.toString()
|
||||
?.let {
|
||||
val mimeType = HttpUtil.getMimeType(msg)?.toString()
|
||||
CachePutRequest(key, CacheValueMetadata(msg.headers().get(HttpHeaderNames.CONTENT_DISPOSITION), mimeType))
|
||||
}
|
||||
?.let(ctx::fireChannelRead)
|
||||
?: ctx.channel().write(CacheValueNotFoundResponse())
|
||||
?: ctx.channel().write(CacheValueNotFoundResponse(key))
|
||||
} else {
|
||||
cacheRequestInProgress = false
|
||||
log.warn(ctx) {
|
||||
"Got request for unhandled path '${msg.uri()}'"
|
||||
}
|
||||
@@ -176,8 +207,11 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
ctx.writeAndFlush(response)
|
||||
}
|
||||
} else if (method == HttpMethod.TRACE) {
|
||||
cacheRequestInProgress = false
|
||||
ctx.pipeline().addAfter(NAME, null, TraceHandler)
|
||||
super.channelRead(ctx, msg)
|
||||
} else {
|
||||
cacheRequestInProgress = false
|
||||
log.warn(ctx) {
|
||||
"Got request with unhandled method '${msg.method().name()}'"
|
||||
}
|
||||
@@ -187,43 +221,7 @@ class ServerHandler(private val serverPrefix: Path) :
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class ContentDisposition(val type: Type?, val fileName: String?) {
|
||||
enum class Type {
|
||||
attachment, `inline`;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun parse(maybeString: String?) = maybeString.let { s ->
|
||||
try {
|
||||
java.lang.Enum.valueOf(Type::class.java, s)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun parse(contentDisposition: String) : ContentDisposition {
|
||||
val parts = contentDisposition.split(";").dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val dispositionType = parts[0].trim { it <= ' ' }.let(Type::parse) // Get the type (e.g., attachment)
|
||||
|
||||
var filename: String? = null
|
||||
for (i in 1..<parts.size) {
|
||||
val part = parts[i].trim { it <= ' ' }
|
||||
if (part.lowercase(Locale.getDefault()).startsWith("filename=")) {
|
||||
filename = part.substring("filename=".length).trim { it <= ' ' }.replace("\"", "")
|
||||
break
|
||||
}
|
||||
}
|
||||
return ContentDisposition(dispositionType, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
super.exceptionCaught(ctx, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -42,6 +42,7 @@ object TraceHandler : ChannelInboundHandlerAdapter() {
|
||||
}
|
||||
is LastHttpContent -> {
|
||||
ctx.writeAndFlush(msg)
|
||||
ctx.pipeline().remove(this)
|
||||
}
|
||||
is HttpContent -> ctx.writeAndFlush(msg)
|
||||
else -> super.channelRead(ctx, msg)
|
||||
|
@@ -1,11 +1,11 @@
|
||||
package net.woggioni.rbcs.server.throttling
|
||||
|
||||
import net.woggioni.jwo.Bucket
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.Arrays
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.function.Function
|
||||
import net.woggioni.jwo.Bucket
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
|
||||
class BucketManager private constructor(
|
||||
private val bucketsByUser: Map<Configuration.User, List<Bucket>> = HashMap(),
|
||||
|
@@ -1,32 +1,50 @@
|
||||
package net.woggioni.rbcs.server.throttling
|
||||
|
||||
import io.netty.buffer.ByteBufHolder
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
import io.netty.handler.codec.http.DefaultFullHttpResponse
|
||||
import io.netty.handler.codec.http.HttpContent
|
||||
import io.netty.handler.codec.http.FullHttpMessage
|
||||
import io.netty.handler.codec.http.HttpHeaderNames
|
||||
import io.netty.handler.codec.http.HttpRequest
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import io.netty.handler.codec.http.HttpVersion
|
||||
import io.netty.handler.codec.http.LastHttpContent
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.ArrayDeque
|
||||
import java.util.concurrent.TimeUnit
|
||||
import net.woggioni.jwo.Bucket
|
||||
import net.woggioni.jwo.LongMath
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class ThrottlingHandler(private val bucketManager : BucketManager,
|
||||
private val connectionConfiguration : Configuration.Connection) : ChannelInboundHandlerAdapter() {
|
||||
class ThrottlingHandler(
|
||||
private val bucketManager: BucketManager,
|
||||
rateLimiterConfiguration: Configuration.RateLimiter,
|
||||
connectionConfiguration: Configuration.Connection
|
||||
) : ChannelInboundHandlerAdapter() {
|
||||
|
||||
private companion object {
|
||||
private val log = createLogger<ThrottlingHandler>()
|
||||
|
||||
fun nextAttemptIsWithinThreshold(nextAttemptNanos : Long, waitThreshold : Duration) : Boolean {
|
||||
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttemptNanos, 100_000_000L) * 100L, ChronoUnit.MILLIS)
|
||||
return waitDuration < waitThreshold
|
||||
}
|
||||
}
|
||||
|
||||
private var queuedContent : MutableList<HttpContent>? = null
|
||||
private class RefusedRequest
|
||||
|
||||
private val maxMessageBufferSize = rateLimiterConfiguration.messageBufferSize
|
||||
private val maxQueuedMessages = rateLimiterConfiguration.maxQueuedMessages
|
||||
private val delayRequests = rateLimiterConfiguration.isDelayRequest
|
||||
private var requestBufferSize : Int = 0
|
||||
private var valveClosed = false
|
||||
private var queuedContent = ArrayDeque<Any>()
|
||||
|
||||
/**
|
||||
* If the suggested waiting time from the bucket is lower than this
|
||||
@@ -39,38 +57,149 @@ class ThrottlingHandler(private val bucketManager : BucketManager,
|
||||
connectionConfiguration.writeIdleTimeout
|
||||
).dividedBy(2)
|
||||
|
||||
|
||||
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if(msg is HttpRequest) {
|
||||
val buckets = mutableListOf<Bucket>()
|
||||
val user = ctx.channel().attr(RemoteBuildCacheServer.userAttribute).get()
|
||||
if (user != null) {
|
||||
bucketManager.getBucketByUser(user)?.let(buckets::addAll)
|
||||
if(valveClosed) {
|
||||
if(msg !is HttpRequest && msg is ByteBufHolder) {
|
||||
val newBufferSize = requestBufferSize + msg.content().readableBytes()
|
||||
if(newBufferSize > maxMessageBufferSize || queuedContent.size + 1 > maxQueuedMessages) {
|
||||
log.debug {
|
||||
if (newBufferSize > maxMessageBufferSize) {
|
||||
"New message part exceeds maxMessageBufferSize, removing previous chunks"
|
||||
} else {
|
||||
"New message part exceeds maxQueuedMessages, removing previous chunks"
|
||||
}
|
||||
}
|
||||
// If this message overflows the maxMessageBufferSize,
|
||||
// then remove the previously enqueued chunks of the request from the deque,
|
||||
// then discard the message
|
||||
while(true) {
|
||||
val tail = queuedContent.last()
|
||||
if(tail is ByteBufHolder) {
|
||||
requestBufferSize -= tail.content().readableBytes()
|
||||
tail.release()
|
||||
}
|
||||
queuedContent.removeLast()
|
||||
if(tail is HttpRequest) {
|
||||
break
|
||||
}
|
||||
}
|
||||
msg.release()
|
||||
//Add a placeholder to remember to return a 429 response corresponding to this request
|
||||
queuedContent.addLast(RefusedRequest())
|
||||
} else {
|
||||
//If the message does not overflow maxMessageBufferSize, just add it to the deque
|
||||
queuedContent.addLast(msg)
|
||||
requestBufferSize = newBufferSize
|
||||
}
|
||||
} else if(msg is HttpRequest && msg is FullHttpMessage){
|
||||
val newBufferSize = requestBufferSize + msg.content().readableBytes()
|
||||
|
||||
// If this message overflows the maxMessageBufferSize,
|
||||
// discard the message
|
||||
if(newBufferSize > maxMessageBufferSize || queuedContent.size + 1 > maxQueuedMessages) {
|
||||
log.debug {
|
||||
if (newBufferSize > maxMessageBufferSize) {
|
||||
"New message exceeds maxMessageBufferSize, discarding it"
|
||||
} else {
|
||||
"New message exceeds maxQueuedMessages, discarding it"
|
||||
}
|
||||
}
|
||||
msg.release()
|
||||
//Add a placeholder to remember to return a 429 response corresponding to this request
|
||||
queuedContent.addLast(RefusedRequest())
|
||||
} else {
|
||||
//If the message does not exceed maxMessageBufferSize or maxQueuedMessages, just add it to the deque
|
||||
queuedContent.addLast(msg)
|
||||
requestBufferSize = newBufferSize
|
||||
}
|
||||
} else {
|
||||
queuedContent.addLast(msg)
|
||||
}
|
||||
val groups = ctx.channel().attr(RemoteBuildCacheServer.groupAttribute).get() ?: emptySet()
|
||||
if (groups.isNotEmpty()) {
|
||||
groups.forEach { group ->
|
||||
bucketManager.getBucketByGroup(group)?.let(buckets::add)
|
||||
} else {
|
||||
entryPoint(ctx, msg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun entryPoint(ctx : ChannelHandlerContext, msg : Any) {
|
||||
if(msg is RefusedRequest) {
|
||||
sendThrottledResponse(ctx, null)
|
||||
if(queuedContent.isEmpty()) {
|
||||
valveClosed = false
|
||||
} else {
|
||||
val head = queuedContent.poll()
|
||||
if(head is ByteBufHolder) {
|
||||
requestBufferSize -= head.content().readableBytes()
|
||||
}
|
||||
entryPoint(ctx, head)
|
||||
}
|
||||
} else if(msg is HttpRequest) {
|
||||
val nextAttempt = getNextAttempt(ctx)
|
||||
if (nextAttempt < 0) {
|
||||
super.channelRead(ctx, msg)
|
||||
if(msg !is LastHttpContent) {
|
||||
while (true) {
|
||||
val head = queuedContent.poll() ?: break
|
||||
if(head is ByteBufHolder) {
|
||||
requestBufferSize -= head.content().readableBytes()
|
||||
}
|
||||
super.channelRead(ctx, head)
|
||||
if (head is LastHttpContent) break
|
||||
}
|
||||
}
|
||||
if(queuedContent.isEmpty()) {
|
||||
valveClosed = false
|
||||
} else {
|
||||
val head = queuedContent.poll()
|
||||
if(head is ByteBufHolder) {
|
||||
requestBufferSize -= head.content().readableBytes()
|
||||
}
|
||||
entryPoint(ctx, head)
|
||||
}
|
||||
} else {
|
||||
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttempt, 100_000_000L) * 100L, ChronoUnit.MILLIS)
|
||||
if (delayRequests && nextAttemptIsWithinThreshold(nextAttempt, waitThreshold)) {
|
||||
valveClosed = true
|
||||
ctx.executor().schedule({
|
||||
entryPoint(ctx, msg)
|
||||
}, waitDuration.toMillis(), TimeUnit.MILLISECONDS)
|
||||
} else {
|
||||
sendThrottledResponse(ctx, waitDuration)
|
||||
if(queuedContent.isEmpty()) {
|
||||
valveClosed = false
|
||||
} else {
|
||||
val head = queuedContent.poll()
|
||||
if(head is ByteBufHolder) {
|
||||
requestBufferSize -= head.content().readableBytes()
|
||||
}
|
||||
entryPoint(ctx, head)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (user == null && groups.isEmpty()) {
|
||||
bucketManager.getBucketByAddress(ctx.channel().remoteAddress() as InetSocketAddress)?.let(buckets::add)
|
||||
}
|
||||
if (buckets.isEmpty()) {
|
||||
super.channelRead(ctx, msg)
|
||||
} else {
|
||||
handleBuckets(buckets, ctx, msg, true)
|
||||
}
|
||||
ctx.channel().id()
|
||||
} else if(msg is HttpContent) {
|
||||
queuedContent?.add(msg) ?: super.channelRead(ctx, msg)
|
||||
} else {
|
||||
super.channelRead(ctx, msg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBuckets(buckets: List<Bucket>, ctx: ChannelHandlerContext, msg: Any, delayResponse: Boolean) {
|
||||
/**
|
||||
* Returns the number amount of milliseconds to wait before the requests can be processed
|
||||
* or -1 if the request can be performed immediately
|
||||
*/
|
||||
private fun getNextAttempt(ctx : ChannelHandlerContext) : Long {
|
||||
val buckets = mutableListOf<Bucket>()
|
||||
val user = ctx.channel().attr(RemoteBuildCacheServer.userAttribute).get()
|
||||
if (user != null) {
|
||||
bucketManager.getBucketByUser(user)?.let(buckets::addAll)
|
||||
}
|
||||
val groups = ctx.channel().attr(RemoteBuildCacheServer.groupAttribute).get() ?: emptySet()
|
||||
if (groups.isNotEmpty()) {
|
||||
groups.forEach { group ->
|
||||
bucketManager.getBucketByGroup(group)?.let(buckets::add)
|
||||
}
|
||||
}
|
||||
if (user == null && groups.isEmpty()) {
|
||||
bucketManager.getBucketByAddress(ctx.channel().remoteAddress() as InetSocketAddress)?.let(buckets::add)
|
||||
}
|
||||
|
||||
var nextAttempt = -1L
|
||||
for (bucket in buckets) {
|
||||
val bucketNextAttempt = bucket.removeTokensWithEstimate(1)
|
||||
@@ -78,38 +207,19 @@ class ThrottlingHandler(private val bucketManager : BucketManager,
|
||||
nextAttempt = bucketNextAttempt
|
||||
}
|
||||
}
|
||||
if (nextAttempt < 0) {
|
||||
super.channelRead(ctx, msg)
|
||||
queuedContent?.let {
|
||||
for(content in it) {
|
||||
super.channelRead(ctx, content)
|
||||
}
|
||||
queuedContent = null
|
||||
}
|
||||
} else {
|
||||
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttempt, 100_000_000L) * 100L, ChronoUnit.MILLIS)
|
||||
if (delayResponse && waitDuration < waitThreshold) {
|
||||
this.queuedContent = mutableListOf()
|
||||
ctx.executor().schedule({
|
||||
handleBuckets(buckets, ctx, msg, false)
|
||||
}, waitDuration.toMillis(), TimeUnit.MILLISECONDS)
|
||||
} else {
|
||||
this.queuedContent = null
|
||||
sendThrottledResponse(ctx, waitDuration)
|
||||
}
|
||||
}
|
||||
return nextAttempt
|
||||
}
|
||||
|
||||
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration) {
|
||||
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration?) {
|
||||
val response = DefaultFullHttpResponse(
|
||||
HttpVersion.HTTP_1_1,
|
||||
HttpResponseStatus.TOO_MANY_REQUESTS
|
||||
)
|
||||
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
|
||||
retryAfter.seconds.takeIf {
|
||||
retryAfter?.seconds?.takeIf {
|
||||
it > 0
|
||||
}?.let {
|
||||
response.headers()[HttpHeaderNames.RETRY_AFTER] = retryAfter.seconds
|
||||
response.headers()[HttpHeaderNames.RETRY_AFTER] = it
|
||||
}
|
||||
|
||||
ctx.writeAndFlush(response)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<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.xsd">
|
||||
xs:schemaLocation="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"/>
|
||||
<cache xs:type="rbcs:fileSystemCacheType" path="${sys:java.io.tmpdir}/rbcs" max-age="P7D"/>
|
||||
</rbcs:server>
|
@@ -16,6 +16,7 @@
|
||||
<xs:element name="bind" type="rbcs:bindType" maxOccurs="1"/>
|
||||
<xs:element name="connection" type="rbcs:connectionType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="event-executor" type="rbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="rate-limiter" type="rbcs:rateLimiterType" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="cache" type="rbcs:cacheType" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
@@ -115,6 +116,14 @@
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Maximum byte size of socket write calls
|
||||
(reduce it to reduce memory consumption, increase it for increased throughput)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="eventExecutorType">
|
||||
@@ -128,6 +137,37 @@
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="rateLimiterType">
|
||||
<xs:attribute name="delay-response" type="xs:boolean" use="optional" default="false">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
If set to true, the server will delay responses to meet user quotas, otherwise it will simply
|
||||
return an immediate 429 status code to all requests that exceed the configured quota
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-queued-messages" type="xs:nonNegativeInteger" use="optional" default="100">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Only meaningful when "delay-response" is set to "true",
|
||||
when a request is delayed, it and all the following messages are queued
|
||||
as long as "max-queued-messages" is not crossed, all requests that would exceed the
|
||||
max-queued-message limit are instead discarded and responded with a 429 status code
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="message-buffer-size" type="rbcs:byteSizeType" use="optional" default="0x100000">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Only meaningful when "delay-response" is set to "true",
|
||||
when a request is delayed, it and all the following requests are buffered
|
||||
as long as "message-buffer-size" is not crossed, all requests that would exceed the buffer
|
||||
size are instead discarded and responded with a 429 status code
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="cacheType" abstract="true"/>
|
||||
|
||||
<xs:complexType name="inMemoryCacheType">
|
||||
@@ -175,13 +215,6 @@
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Maximum byte size of socket write calls
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
@@ -231,14 +264,6 @@
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Maximum byte size of a cache value that will be stored in memory
|
||||
(reduce it to reduce memory consumption, increase it for increased throughput)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
@@ -424,7 +449,8 @@
|
||||
<xs:attribute name="password" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
User's password used in HTTP basic authentication
|
||||
User's password hash used for HTTP basic authentication, this has to be generated with
|
||||
the `password` subcommand of `rbcs-cli`
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
@@ -498,18 +524,12 @@
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="role" final="restriction" >
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:enumeration value="READER" />
|
||||
<xs:enumeration value="WRITER" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="rolesType">
|
||||
<xs:sequence>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="writer"/>
|
||||
<xs:element name="reader"/>
|
||||
<xs:element name="healthcheck"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
@@ -1,30 +0,0 @@
|
||||
package net.woggioni.rbcs.server.test.utils;
|
||||
|
||||
import net.woggioni.jwo.JWO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
public class NetworkUtils {
|
||||
|
||||
private static final int MAX_ATTEMPTS = 50;
|
||||
|
||||
public static int getFreePort() {
|
||||
int count = 0;
|
||||
while(count < MAX_ATTEMPTS) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(0, 50, InetAddress.getLocalHost())) {
|
||||
final var candidate = serverSocket.getLocalPort();
|
||||
if (candidate > 0) {
|
||||
return candidate;
|
||||
} else {
|
||||
JWO.newThrowable(RuntimeException.class, "Got invalid port number: %d", candidate);
|
||||
throw new RuntimeException("Error trying to find an open port");
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Error trying to find an open port");
|
||||
}
|
||||
}
|
@@ -1,11 +1,5 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.Role
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
import net.woggioni.rbcs.server.test.utils.NetworkUtils
|
||||
import java.net.URI
|
||||
import java.net.http.HttpRequest
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -15,6 +9,12 @@ import java.time.temporal.ChronoUnit
|
||||
import java.util.Base64
|
||||
import java.util.zip.Deflater
|
||||
import kotlin.random.Random
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.Role
|
||||
import net.woggioni.rbcs.common.RBCS.getFreePort
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
|
||||
|
||||
abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
||||
@@ -33,15 +33,17 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
||||
this.cacheDir = testDir.resolve("cache")
|
||||
cfg = Configuration.of(
|
||||
"127.0.0.1",
|
||||
NetworkUtils.getFreePort(),
|
||||
getFreePort(),
|
||||
50,
|
||||
serverPath,
|
||||
Configuration.EventExecutor(false),
|
||||
Configuration.RateLimiter(true, 0x100000, 50),
|
||||
Configuration.Connection(
|
||||
Duration.of(60, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
0x1000
|
||||
0x1000,
|
||||
0x10000
|
||||
),
|
||||
users.asSequence().map { it.name to it}.toMap(),
|
||||
sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
|
||||
@@ -50,8 +52,7 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
||||
maxAge = Duration.ofSeconds(3600 * 24),
|
||||
digestAlgorithm = "MD5",
|
||||
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
||||
compressionEnabled = false,
|
||||
chunkSize = 0x1000
|
||||
compressionEnabled = false
|
||||
),
|
||||
Configuration.BasicAuthentication(),
|
||||
null,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import java.nio.file.Path
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
@@ -8,7 +9,6 @@ import org.junit.jupiter.api.MethodOrderer
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.TestMethodOrder
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
|
@@ -1,14 +1,5 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.Role
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
import net.woggioni.rbcs.server.test.utils.CertificateUtils
|
||||
import net.woggioni.rbcs.server.test.utils.CertificateUtils.X509Credentials
|
||||
import net.woggioni.rbcs.server.test.utils.NetworkUtils
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.net.URI
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
@@ -25,6 +16,15 @@ import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import kotlin.random.Random
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.Role
|
||||
import net.woggioni.rbcs.common.RBCS.getFreePort
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
import net.woggioni.rbcs.server.test.utils.CertificateUtils
|
||||
import net.woggioni.rbcs.server.test.utils.CertificateUtils.X509Credentials
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
|
||||
|
||||
abstract class AbstractTlsServerTest : AbstractServerTest() {
|
||||
@@ -47,6 +47,7 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
|
||||
|
||||
protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader), null, null)
|
||||
protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer), null, null)
|
||||
protected val healthCheckGroup = Configuration.Group("healthcheckers", setOf(Role.Healthcheck), null, null)
|
||||
protected val random = Random(101325)
|
||||
protected val keyValuePair = newEntry(random)
|
||||
private val serverPath : String? = null
|
||||
@@ -138,15 +139,17 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
|
||||
createKeyStoreAndTrustStore()
|
||||
cfg = Configuration(
|
||||
"127.0.0.1",
|
||||
NetworkUtils.getFreePort(),
|
||||
getFreePort(),
|
||||
100,
|
||||
serverPath,
|
||||
Configuration.EventExecutor(false),
|
||||
Configuration.RateLimiter(true, 0x100000, 50),
|
||||
Configuration.Connection(
|
||||
Duration.of(60, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
0x1000
|
||||
0x1000,
|
||||
0x10000
|
||||
),
|
||||
users.asSequence().map { it.name to it }.toMap(),
|
||||
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
|
||||
@@ -155,7 +158,6 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
|
||||
compressionEnabled = false,
|
||||
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
||||
digestAlgorithm = "MD5",
|
||||
chunkSize = 0x1000
|
||||
),
|
||||
// InMemoryCacheConfiguration(
|
||||
// maxAge = Duration.ofSeconds(3600 * 24),
|
||||
|
@@ -1,17 +1,17 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.Role
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Order
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
|
||||
class BasicAuthServerTest : AbstractBasicAuthServerTest() {
|
||||
@@ -154,7 +154,7 @@ class BasicAuthServerTest : AbstractBasicAuthServerTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@Order(8)
|
||||
fun getAsAThrottledUser() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
@@ -172,7 +172,7 @@ class BasicAuthServerTest : AbstractBasicAuthServerTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@Order(9)
|
||||
fun getAsAThrottledUser2() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import net.woggioni.rbcs.common.RBCS.toUrl
|
||||
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
@@ -10,8 +12,6 @@ import org.junit.jupiter.api.io.TempDir
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
import org.xml.sax.SAXParseException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class ConfigurationTest {
|
||||
|
||||
|
@@ -1,14 +1,14 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Order
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
|
||||
|
||||
class NoAnonymousUserBasicAuthServerTest : AbstractBasicAuthServerTest() {
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Order
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
|
||||
class NoAnonymousUserTlsServerTest : AbstractTlsServerTest() {
|
||||
|
||||
|
@@ -1,14 +1,6 @@
|
||||
package net.woggioni.rbcs.server.test
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
import net.woggioni.rbcs.server.test.utils.NetworkUtils
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Order
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.URI
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
@@ -19,6 +11,14 @@ import java.time.temporal.ChronoUnit
|
||||
import java.util.Base64
|
||||
import java.util.zip.Deflater
|
||||
import kotlin.random.Random
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.common.RBCS.getFreePort
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
|
||||
import net.woggioni.rbcs.server.configuration.Serializer
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Order
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
||||
class NoAuthServerTest : AbstractServerTest() {
|
||||
@@ -33,15 +33,17 @@ class NoAuthServerTest : AbstractServerTest() {
|
||||
this.cacheDir = testDir.resolve("cache")
|
||||
cfg = Configuration(
|
||||
"127.0.0.1",
|
||||
NetworkUtils.getFreePort(),
|
||||
getFreePort(),
|
||||
100,
|
||||
serverPath,
|
||||
Configuration.EventExecutor(false),
|
||||
Configuration.RateLimiter(true, 0x100000, 50),
|
||||
Configuration.Connection(
|
||||
Duration.of(60, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
Duration.of(30, ChronoUnit.SECONDS),
|
||||
0x1000
|
||||
0x1000,
|
||||
0x10000
|
||||
),
|
||||
emptyMap(),
|
||||
emptyMap(),
|
||||
@@ -51,7 +53,6 @@ class NoAuthServerTest : AbstractServerTest() {
|
||||
digestAlgorithm = "MD5",
|
||||
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
||||
maxSize = 0x1000000,
|
||||
chunkSize = 0x1000
|
||||
),
|
||||
null,
|
||||
null,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user