Compare commits
48 Commits
0.2.0-alph
...
559ad5e528
Author | SHA1 | Date | |
---|---|---|---|
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
|
|||
23f2a351a6
|
|||
c7d2b89d82
|
|||
72c34b57a6
|
|||
619873c4a9
|
|||
591f6e2af4
|
|||
ad00ebee9b
|
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: hostinger
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
- name: Execute Gradle build
|
||||||
|
run: ./gradlew build
|
||||||
|
- name: Prepare Docker image build
|
||||||
|
run: ./gradlew prepareDockerBuild
|
||||||
|
- name: Get project version
|
||||||
|
id: retrieve-version
|
||||||
|
run: ./gradlew -q version >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: docker-container
|
||||||
|
- name: Login to Gitea container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.woggioni.net
|
||||||
|
username: woggioni
|
||||||
|
password: ${{ secrets.PUBLISHER_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Build rbcs Docker image
|
||||||
|
uses: docker/build-push-action@v5.3.0
|
||||||
|
with:
|
||||||
|
context: "docker/build/docker"
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
tags: |
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:vanilla-dev
|
||||||
|
target: release-vanilla
|
||||||
|
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||||
|
-
|
||||||
|
name: Build rbcs memcache Docker image
|
||||||
|
uses: docker/build-push-action@v5.3.0
|
||||||
|
with:
|
||||||
|
context: "docker/build/docker"
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
tags: |
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:memcache-dev
|
||||||
|
target: release-memcache
|
||||||
|
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||||
|
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||||
|
-
|
||||||
|
name: Build rbcs native Docker image
|
||||||
|
uses: docker/build-push-action@v5.3.0
|
||||||
|
with:
|
||||||
|
context: "docker/build/docker"
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
tags: |
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:native-dev
|
||||||
|
target: release-native
|
||||||
|
-
|
||||||
|
name: Build rbcs jlink Docker image
|
||||||
|
uses: docker/build-push-action@v5.3.0
|
||||||
|
with:
|
||||||
|
context: "docker/build/docker"
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
tags: |
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:jlink-dev
|
||||||
|
target: release-jlink
|
||||||
|
|
@@ -39,9 +39,9 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
pull: true
|
pull: true
|
||||||
tags: |
|
tags: |
|
||||||
gitea.woggioni.net/woggioni/rbcs:latest
|
gitea.woggioni.net/woggioni/rbcs:vanilla
|
||||||
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
|
gitea.woggioni.net/woggioni/rbcs:vanilla-${{ steps.retrieve-version.outputs.VERSION }}
|
||||||
target: release
|
target: release-vanilla
|
||||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||||
-
|
-
|
||||||
name: Build rbcs memcache Docker image
|
name: Build rbcs memcache Docker image
|
||||||
@@ -52,11 +52,37 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
pull: true
|
pull: true
|
||||||
tags: |
|
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
|
||||||
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
|
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
|
||||||
target: release-memcache
|
target: release-memcache
|
||||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
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
|
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||||
|
-
|
||||||
|
name: Build rbcs native Docker image
|
||||||
|
uses: docker/build-push-action@v5.3.0
|
||||||
|
with:
|
||||||
|
context: "docker/build/docker"
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
tags: |
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:native
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:native-${{ steps.retrieve-version.outputs.VERSION }}
|
||||||
|
target: release-native
|
||||||
|
-
|
||||||
|
name: Build rbcs jlink Docker image
|
||||||
|
uses: docker/build-push-action@v5.3.0
|
||||||
|
with:
|
||||||
|
context: "docker/build/docker"
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
tags: |
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:jlink
|
||||||
|
gitea.woggioni.net/woggioni/rbcs:jlink-${{ steps.retrieve-version.outputs.VERSION }}-jlink
|
||||||
|
target: release-jlink
|
||||||
- name: Publish artifacts
|
- name: Publish artifacts
|
||||||
env:
|
env:
|
||||||
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
|
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
|
||||||
|
20
LICENSE
Normal file
20
LICENSE
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Y. T. CHUNG <zonyitoo@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
433
README.md
433
README.md
@@ -0,0 +1,433 @@
|
|||||||
|
# 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
|
||||||
|
Maven build tool environments.
|
||||||
|
|
||||||
|
It comes with pluggable storage backends, the core application offers in-memory storage or disk-backed storage,
|
||||||
|
in addition to this there is an official plugin to use memcached as the storage backend.
|
||||||
|
|
||||||
|
It supports HTTP basic authentication or, alternatively, TLS certificate authentication, role-based access control (RBAC),
|
||||||
|
and throttling.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
### Use the all-in-one jar file
|
||||||
|
You can download the latest version from [this link](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/)
|
||||||
|
|
||||||
|
|
||||||
|
Assuming you have Java 21 or later installed, you can launch the server directly with
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar rbcs-cli.jar server
|
||||||
|
```
|
||||||
|
|
||||||
|
By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication,
|
||||||
|
writing data to the disk, that you can use for testing
|
||||||
|
|
||||||
|
### Use the Docker image
|
||||||
|
You can pull the latest Docker image with
|
||||||
|
```bash
|
||||||
|
docker pull gitea.woggioni.net/woggioni/rbcs:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
By default it will start an HTTP server bound to localhost and listening on port 8080 with no authentication,
|
||||||
|
writing data to the disk, that you can use for testing
|
||||||
|
|
||||||
|
### Use the native executable
|
||||||
|
If you are on a Linux X86_64 machine you can download the native executable
|
||||||
|
from [here](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-cli/).
|
||||||
|
It behaves the same as the jar file but it doesn't require a JVM and it has faster startup times.
|
||||||
|
because of GraalVM's [closed-world assumption](https://www.graalvm.org/latest/reference-manual/native-image/basics/#static-analysis),
|
||||||
|
the native executable does not supports plugins, so it comes with all plugins embedded into it.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The native executable is built with `-march=skylake`, so it may fail with SIGILL on x86 CPUs that do not support
|
||||||
|
> the full skylake instruction set (as a rule of thumb, older than 2015)
|
||||||
|
|
||||||
|
## Integration with build tools
|
||||||
|
|
||||||
|
### Use RBCS with Gradle
|
||||||
|
|
||||||
|
Add this to the `settings.gradle` file of your project
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
buildCache {
|
||||||
|
remote(HttpBuildCache) {
|
||||||
|
url = 'https://rbcs.example.com/'
|
||||||
|
push = true
|
||||||
|
allowInsecureProtocol = false
|
||||||
|
// The credentials block is only required if you enable
|
||||||
|
// HTTP basic authentication on RBCS
|
||||||
|
credentials {
|
||||||
|
username = 'build-cache-user'
|
||||||
|
password = 'some-complicated-password'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
alternatively you can add this to `${GRADLE_HOME}/init.gradle` to configure the remote cache
|
||||||
|
at the system level
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
gradle.settingsEvaluated { settings ->
|
||||||
|
settings.buildCache {
|
||||||
|
remote(HttpBuildCache) {
|
||||||
|
url = 'https://rbcs.example.com/'
|
||||||
|
push = true
|
||||||
|
allowInsecureProtocol = false
|
||||||
|
// The credentials block is only required if you enable
|
||||||
|
// HTTP basic authentication on RBCS
|
||||||
|
credentials {
|
||||||
|
username = 'build-cache-user'
|
||||||
|
password = 'some-complicated-password'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
add `org.gradle.caching=true` to your `<project>/gradle.properties` or run gradle with `--build-cache`.
|
||||||
|
|
||||||
|
Read [Gradle documentation](https://docs.gradle.org/current/userguide/build_cache.html) for more detailed information.
|
||||||
|
|
||||||
|
### Use RBCS with Maven
|
||||||
|
|
||||||
|
1. Create an `extensions.xml` in `<project>/.mvn/extensions.xml` with the following content
|
||||||
|
```xml
|
||||||
|
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.1.0 https://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
|
||||||
|
<extension>
|
||||||
|
<groupId>org.apache.maven.extensions</groupId>
|
||||||
|
<artifactId>maven-build-cache-extension</artifactId>
|
||||||
|
<version>1.2.0</version>
|
||||||
|
</extension>
|
||||||
|
</extensions>
|
||||||
|
```
|
||||||
|
2. Copy [maven-build-cache-config.xml](https://maven.apache.org/extensions/maven-build-cache-extension/maven-build-cache-config.xml) into `<project>/.mvn/` folder
|
||||||
|
3. Edit the `cache/configuration/remote` element
|
||||||
|
```xml
|
||||||
|
<remote enabled="true" id="rbcs">
|
||||||
|
<url>https://rbcs.example.com/</url>
|
||||||
|
</remote>
|
||||||
|
```
|
||||||
|
4. Run maven with
|
||||||
|
```bash
|
||||||
|
mvn -Dmaven.build.cache.enabled=true -Dmaven.build.cache.debugOutput=true -Dmaven.build.cache.remote.save.enabled=true package
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively you can set those properties in your `<project>/pom.xml`
|
||||||
|
|
||||||
|
Read [here](https://maven.apache.org/extensions/maven-build-cache-extension/remote-cache.html)
|
||||||
|
for more informations
|
||||||
|
|
||||||
|
|
||||||
|
## Server configuration
|
||||||
|
RBCS reads an XML configuration file, by default named `rbcs-server.xml`.
|
||||||
|
The expected location of the `rbcs-server.xml` file depends on the operating system,
|
||||||
|
if the configuration file is not found a default one will be created and its location is printed
|
||||||
|
on the console
|
||||||
|
|
||||||
|
```bash
|
||||||
|
user@76a90cbcd75d:~$ rbcs-cli server
|
||||||
|
2025-01-01 00:00:00,000 [INFO ] (main) n.w.r.c.impl.commands.ServerCommand -- Creating default configuration file at '/home/user/.config/rbcs/rbcs-server.xml'
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively it can be changed setting the `RBCS_CONFIGURATION_DIR` environmental variable or `net.woggioni.rbcs.conf.dir`
|
||||||
|
Java system property to the directory that contain the `rbcs-server.xml` file.
|
||||||
|
It can also be directly specified from the command line with
|
||||||
|
```bash
|
||||||
|
java -jar rbcs-cli.jar server -c /path/to/rbcs-server.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
The server configuration file follows the XML format and uses XML schema for validation
|
||||||
|
(you can find the schema for the `rbcs-server.xml` configuration file [here](https://gitea.woggioni.net/woggioni/rbcs/src/branch/master/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd)).
|
||||||
|
|
||||||
|
The configuration values are enclosed inside XML attribute and support system property / environmental variable interpolation.
|
||||||
|
As an example, you can configure RBCS to read the server port number from the `RBCS_SERVER_PORT` environmental variable
|
||||||
|
and the bind address from the `rbc.bind.address` JVM system property with
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<bind host="${sys:rpc.bind.address}" port="${env:RBCS_SERVER_PORT}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Full documentation for all tags and attributes and configuration file examples
|
||||||
|
are available [here](doc/server_configuration.md).
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
If you want to use memcache as a storage backend you'll also need to download [the memcache plugin](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-server-memcache/)
|
||||||
|
|
||||||
|
Plugins need to be stored in a folder named `plugins` in the located server's working directory
|
||||||
|
(the directory where the server process is started). They are shipped as TAR archives, so you need to extract
|
||||||
|
the content of the archive into the `plugins` directory for the server to pick them up.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
RBCS supports 2 authentication mechanisms:
|
||||||
|
|
||||||
|
- HTTP basic authentication
|
||||||
|
- TLS certificate authentication
|
||||||
|
|
||||||
|
### Configure HTTP basic authentication
|
||||||
|
|
||||||
|
Add a `<basic>` element to the `<authentication>` element in your `rbcs-server.xml`
|
||||||
|
```xml
|
||||||
|
<authentication>
|
||||||
|
<basic/>
|
||||||
|
</authentication>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure TLS certificate authentication
|
||||||
|
|
||||||
|
Add a `<client-certificate>` element to the `<authentication>` element in your `rbcs-server.xml`
|
||||||
|
```xml
|
||||||
|
<authentication>
|
||||||
|
<client-certificate>
|
||||||
|
<user-extractor attribute-name="CN" pattern="(.*)"/>
|
||||||
|
<group-extractor attribute-name="O" pattern="(.*)"/>
|
||||||
|
</client-certificate>
|
||||||
|
</authentication>
|
||||||
|
```
|
||||||
|
The `<user-extractor>` here determines how the username is extracted from the
|
||||||
|
subject's X.500 name in the TLS certificate presented by the client, where `attribute-name`
|
||||||
|
is the `RelativeDistinguishedName` (RDN) identifier and pattern is a regular expression
|
||||||
|
that will be applied to extract the username from the first group present in the regex.
|
||||||
|
An error will be thrown if the regular expression contains no groups, while additional
|
||||||
|
groups are ignored.
|
||||||
|
|
||||||
|
Similarly, the `<group-extractor>` here determines how the group name is extracted from the
|
||||||
|
subject's X.500 name in the TLS certificate presented by the client.
|
||||||
|
Note that this allows to assign roles to incoming requests without necessarily assigning them
|
||||||
|
a username.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Access control
|
||||||
|
|
||||||
|
RBCS supports role-based access control (RBAC), three roles are available:
|
||||||
|
- `Reader` can perform `GET` calls
|
||||||
|
- `Writer` can perform `PUT` calls
|
||||||
|
- `Healthcheck` can perform `TRACE` calls
|
||||||
|
|
||||||
|
Roles are assigned to groups so that a user will have a role only if that roles belongs
|
||||||
|
to one of the groups he is a member of.
|
||||||
|
|
||||||
|
There is also a special `<anonymous>` user
|
||||||
|
which matches any request who hasn't been authenticated and that can be assigned
|
||||||
|
to any group like a normal user. This permits to have a build cache that is
|
||||||
|
publicly readable but only writable by authenticated users (e.g. CI/CD pipeline).
|
||||||
|
|
||||||
|
### Defining users
|
||||||
|
|
||||||
|
Users can be defined in the `<authorization>` element
|
||||||
|
```xml
|
||||||
|
<authorization>
|
||||||
|
<users>
|
||||||
|
<user name="user1" password="kb/vNnkn2RvyPkTN6Q07uH0F7wI7u61MkManD3NHregRukBg4KHehfbqtLTb39fZjHA+SRH+EpEWDCf+Rihr5H5C1YN5qwmArV0p8O5ptC4="/>
|
||||||
|
<user name="user2" password="2J7MAhdIzZ3SO+JGB+K6wPhb4P5LH1L4L7yJCl5QrxNfAWRr5jTUExJRbcgbH1UfnkCbIO1p+xTDq+FCj3LFBZeMZUNZ47npN+WR7AX3VTo="/>
|
||||||
|
<anonymous/>
|
||||||
|
</users>
|
||||||
|
<groups>
|
||||||
|
<group name="readers">
|
||||||
|
<users>
|
||||||
|
<anonymous/>
|
||||||
|
</users>
|
||||||
|
<roles>
|
||||||
|
<reader/>
|
||||||
|
</roles>
|
||||||
|
</group>
|
||||||
|
<group name="writers">
|
||||||
|
<users>
|
||||||
|
<user ref="user1"/>
|
||||||
|
<user ref="user2"/>
|
||||||
|
</users>
|
||||||
|
<roles>
|
||||||
|
<reader/>
|
||||||
|
<writer/>
|
||||||
|
<healthcheck/>
|
||||||
|
</roles>
|
||||||
|
</group>
|
||||||
|
</groups>
|
||||||
|
</authorization>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `password` attribute is only used for HTTP Basic authentication, so it can be omitted
|
||||||
|
if you use TLS certificate authentication. It must contain a password hash that can be derived from
|
||||||
|
the actual password using the following command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar rbcs-cli.jar password
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reliability
|
||||||
|
|
||||||
|
RBCS implements the [TRACE](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE) HTTP method and this functionality can be used
|
||||||
|
as a health check (mind you need to have `Healthcheck` role in order to perform it and match the server's `prefix` in the URL).
|
||||||
|
|
||||||
|
## RBCS Client
|
||||||
|
|
||||||
|
RBCS ships with a command line client that can be used for testing, benchmarking or to manually
|
||||||
|
upload/download files to the cache. It must be configured with the `rbcs-client.xml`,
|
||||||
|
whose location follows the same logic of the `rbcs-server.xml`.
|
||||||
|
The `rbcs-client.xml` must adhere to the [rbcs-client.xsd](rbcs-client/src/main/resources/net/woggioni/rbcs/client/schema/rbcs-client.xsd)
|
||||||
|
XML schema
|
||||||
|
|
||||||
|
The documentation for the `rbcs-client.xml` configuration file is available [here](conf/client_configuration.md)
|
||||||
|
|
||||||
|
### GET command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME get -k $CACHE_KEY -v $FILE_WHERE_THE_VALUE_WILL_BE_STORED
|
||||||
|
```
|
||||||
|
|
||||||
|
### PUT command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME put -k $CACHE_KEY -v $FILE_TO_BE_UPLOADED
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't specify the key, a UUID key based on the file content will be used,
|
||||||
|
if you add the `-i` command line parameter, the uploaded file will be served with
|
||||||
|
`Content-Disposition: inline` HTTP header so that browser will attempt to render
|
||||||
|
it in the page instead of triggering a file download (in this way you can create a temporary web page).
|
||||||
|
|
||||||
|
The client will try to detect the file mime type upon upload but if you want to be sure you can specify
|
||||||
|
it manually with the `-t` parameter.
|
||||||
|
|
||||||
|
### Benchmark command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar rbcs-cli.jar client -p $CLIENT_PROFILE_NAME benchamrk -s 4096 -e 10000
|
||||||
|
```
|
||||||
|
This will insert 10000 randomly generates entries of 4096 bytes into RBCS, then retrieve them
|
||||||
|
and check that the retrieved value matches what was inserted.
|
||||||
|
It will also print throughput stats on the way.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
RBCS uses [logback](https://logback.qos.ch/) and ships with a [default logging configuration](./conf/logback.xml) that
|
||||||
|
can be overridden with `-Dlogback.configurationFile=path/to/custom/configuration.xml`, refer to
|
||||||
|
[Logback documentation](https://logback.qos.ch/manual/configuration.html) for more details about
|
||||||
|
how to configure Logback
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
You can check performance benchmarks [here](doc/benchmarks.md)
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
### Why should I use a build cache?
|
||||||
|
|
||||||
|
#### Build Caches Improve Build & Test Performance
|
||||||
|
|
||||||
|
Building software consists of a number of steps, like compiling sources, executing tests, and linking binaries. We’ve seen that a binary artifact repository helps when such a step requires an external component by downloading the artifact from the repository rather than building it locally.
|
||||||
|
However, there are many additional steps in this build process which can be optimized to reduce the build time. An obvious strategy is to avoid executing build steps which dominate the total build time when these build steps are not needed.
|
||||||
|
Most build times are dominated by the testing step.
|
||||||
|
|
||||||
|
While binary repositories cannot capture the outcome of a test build step (only the test reports
|
||||||
|
when included in binary artifacts), build caches are designed to eliminate redundant executions
|
||||||
|
for every build step. Moreover, it generalizes the concept of avoiding work associated with any
|
||||||
|
incremental step of the build, including test execution, compilation and resource processing.
|
||||||
|
The mechanism itself is comparable to a pure function. That is, given some inputs such as source
|
||||||
|
files and environment parameters we know that the output is always going to be the same.
|
||||||
|
As a result, we can cache it and retrieve it based on a simple cryptographic hash of the inputs.
|
||||||
|
Build caching is supported natively by some build tools.
|
||||||
|
|
||||||
|
#### Improve CI builds with a remote build cache
|
||||||
|
|
||||||
|
When analyzing the role of a build cache it is important to take into account the granularity
|
||||||
|
of the changes that it caches. Imagine a full build for a project with 40 to 50 modules
|
||||||
|
which fails at the last step (deployment) because the staging environment is temporarily unavailable.
|
||||||
|
Although the vast majority of the build steps (potentially thousands) succeed,
|
||||||
|
the change can not be deployed to the staging environment.
|
||||||
|
Without a build cache one typically relies on a very complex CI configuration to reuse build step outputs
|
||||||
|
or would have to repeat the full build once the environment is available.
|
||||||
|
|
||||||
|
Some build tools don’t support incremental builds properly. For example, outputs of a build started
|
||||||
|
from scratch may vary when compared to subsequent builds that rely on the initial build’s output.
|
||||||
|
As a result, to preserve build integrity, it’s crucial to rebuild from scratch, or ‘cleanly,’ in this
|
||||||
|
scenario.
|
||||||
|
|
||||||
|
With a build cache, only the last step needs to be executed and the build can be re-triggered
|
||||||
|
when the environment is back online. This automatically saves all of the time and
|
||||||
|
resources required across the different build steps which were successfully executed.
|
||||||
|
Instead of executing the intermediate steps, the build tool pulls the outputs from the build cache,
|
||||||
|
avoiding a lot of redundant work
|
||||||
|
|
||||||
|
#### Share outputs with a remote build cache
|
||||||
|
|
||||||
|
One of the most important advantages of a remote build cache is the ability to share build outputs.
|
||||||
|
In most CI configurations, for example, a number of pipelines are created.
|
||||||
|
These may include one for building the sources, one for testing, one for publishing the outcomes
|
||||||
|
to a remote repository, and other pipelines to test on different platforms.
|
||||||
|
There are even situations where CI builds partially build a project (i.e. some modules and not others).
|
||||||
|
|
||||||
|
Most of those pipelines share a lot of intermediate build steps. All builds which perform testing
|
||||||
|
require the binaries to be ready. All publishing builds require all previous steps to be executed.
|
||||||
|
And because modern CI infrastructure means executing everything in containerized (isolated) environments,
|
||||||
|
significant resources are wasted by repeatedly building the same intermediate artifacts.
|
||||||
|
|
||||||
|
A remote build cache greatly reduces this overhead by orders of magnitudes because it provides a way
|
||||||
|
for all those pipelines to share their outputs. After all, there is no point recreating an output that
|
||||||
|
is already available in the cache.
|
||||||
|
|
||||||
|
Because there are inherent dependencies between software components of a build,
|
||||||
|
introducing a build cache dramatically reduces the impact of exploding a component into multiple pieces,
|
||||||
|
allowing for increased modularity without increased overhead.
|
||||||
|
|
||||||
|
#### Make local developers more efficient with remote build caches
|
||||||
|
|
||||||
|
It is common for different teams within a company to work on different modules of a single large
|
||||||
|
application. In this case, most teams don’t care about building the other parts of the software.
|
||||||
|
By introducing a remote cache developers immediately benefit from pre-built artifacts when checking out code.
|
||||||
|
Because it has already been built on CI, they don’t have to do it locally.
|
||||||
|
|
||||||
|
Introducing a remote cache is a huge benefit for those developers. Consider that a typical developer’s
|
||||||
|
day begins by performing a code checkout. Most likely the checked out code has already been built on CI.
|
||||||
|
Therefore, no time is wasted running the first build of the day. The remote cache provides all of the
|
||||||
|
intermediate artifacts needed. And, in the event local changes are made, the remote cache still leverages
|
||||||
|
partial cache hits for projects which are independent. As other developers in the organization request
|
||||||
|
CI builds, the remote cache continues to populate, increasing the likelihood of these remote cache hits
|
||||||
|
across team members.
|
||||||
|
|
||||||
|
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
|
@@ -14,9 +14,7 @@ allprojects { subproject ->
|
|||||||
if(project.currentTag.isPresent()) {
|
if(project.currentTag.isPresent()) {
|
||||||
version = project.currentTag.map { it[0] }.get()
|
version = project.currentTag.map { it[0] }.get()
|
||||||
} else {
|
} else {
|
||||||
version = project.gitRevision.map { gitRevision ->
|
version = "${getProperty('rbcs.version')}-SNAPSHOT"
|
||||||
"${getProperty('rbcs.version')}.${gitRevision[0..10]}"
|
|
||||||
}.get()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -24,7 +22,6 @@ allprojects { subproject ->
|
|||||||
url = getProperty('gitea.maven.url')
|
url = getProperty('gitea.maven.url')
|
||||||
content {
|
content {
|
||||||
includeModule 'net.woggioni', 'jwo'
|
includeModule 'net.woggioni', 'jwo'
|
||||||
includeModule 'net.woggioni', 'xmemcached'
|
|
||||||
includeGroup 'com.lys'
|
includeGroup 'com.lys'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +39,6 @@ allprojects { subproject ->
|
|||||||
modularity.inferModulePath = true
|
modularity.inferModulePath = true
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
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
|
USER luser
|
||||||
WORKDIR /home/luser
|
WORKDIR /home/luser
|
||||||
|
|
||||||
FROM base-release AS release
|
FROM base-release AS release-vanilla
|
||||||
ADD rbcs-cli-envelope-*.jar rbcs.jar
|
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
|
FROM base-release AS release-memcache
|
||||||
ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.jar
|
ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.jar
|
||||||
@@ -13,4 +13,30 @@ RUN mkdir plugins
|
|||||||
WORKDIR /home/luser/plugins
|
WORKDIR /home/luser/plugins
|
||||||
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar
|
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar
|
||||||
WORKDIR /home/luser
|
WORKDIR /home/luser
|
||||||
ENTRYPOINT ["java", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"]
|
ADD logback.xml .
|
||||||
|
ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=70", "-XX:GCTimeRatio=24", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar"]
|
||||||
|
|
||||||
|
FROM 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,10 @@ Provider<Copy> prepareDockerBuild = tasks.register('prepareDockerBuild', Copy) {
|
|||||||
group = 'docker'
|
group = 'docker'
|
||||||
into project.layout.buildDirectory.file('docker')
|
into project.layout.buildDirectory.file('docker')
|
||||||
from(configurations.docker)
|
from(configurations.docker)
|
||||||
from(file('Dockerfile'))
|
from(files('Dockerfile', 'rbcs-cli.sh'))
|
||||||
|
from(rootProject.file('conf')) {
|
||||||
|
include 'logback.xml'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Provider<DockerBuildImage> dockerBuild = tasks.register('dockerBuildImage', DockerBuildImage) {
|
Provider<DockerBuildImage> dockerBuild = tasks.register('dockerBuildImage', DockerBuildImage) {
|
||||||
@@ -63,5 +66,3 @@ Provider<DockerPushImage> dockerPush = tasks.register('dockerPushImage', DockerP
|
|||||||
}
|
}
|
||||||
images = [dockerTag.flatMap{ it.tag }, dockerTagMemcache.flatMap{ it.tag }]
|
images = [dockerTag.flatMap{ it.tag }, dockerTagMemcache.flatMap{ it.tag }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.parallel=true
|
||||||
org.gradle.caching=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
|
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
||||||
docker.registry.url=gitea.woggioni.net
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
3
gradlew
vendored
3
gradlew
vendored
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@@ -5,8 +5,12 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation catalog.slf4j.api
|
||||||
|
implementation project(':rbcs-common')
|
||||||
|
api catalog.netty.common
|
||||||
api catalog.netty.buffer
|
api catalog.netty.buffer
|
||||||
api catalog.netty.handler
|
api catalog.netty.handler
|
||||||
|
api catalog.netty.codec.http
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
@@ -1,9 +1,15 @@
|
|||||||
module net.woggioni.rbcs.api {
|
module net.woggioni.rbcs.api {
|
||||||
requires static lombok;
|
requires static lombok;
|
||||||
requires java.xml;
|
|
||||||
requires io.netty.buffer;
|
|
||||||
requires io.netty.handler;
|
requires io.netty.handler;
|
||||||
|
requires io.netty.common;
|
||||||
|
requires net.woggioni.rbcs.common;
|
||||||
requires io.netty.transport;
|
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;
|
||||||
exports net.woggioni.rbcs.api.exception;
|
exports net.woggioni.rbcs.api.exception;
|
||||||
exports net.woggioni.rbcs.api.message;
|
exports net.woggioni.rbcs.api.message;
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
package net.woggioni.rbcs.api;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface AsyncCloseable extends AutoCloseable {
|
||||||
|
|
||||||
|
CompletableFuture<Void> asyncClose();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void close() throws Exception {
|
||||||
|
asyncClose().get();
|
||||||
|
}
|
||||||
|
}
|
@@ -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,7 +1,15 @@
|
|||||||
package net.woggioni.rbcs.api;
|
package net.woggioni.rbcs.api;
|
||||||
|
|
||||||
import io.netty.channel.ChannelHandler;
|
import io.netty.channel.ChannelFactory;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.socket.DatagramChannel;
|
||||||
|
import io.netty.channel.socket.SocketChannel;
|
||||||
|
|
||||||
public interface CacheHandlerFactory extends AutoCloseable {
|
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
|
@NonNull
|
||||||
EventExecutor eventExecutor;
|
EventExecutor eventExecutor;
|
||||||
@NonNull
|
@NonNull
|
||||||
|
RateLimiter rateLimiter;
|
||||||
|
@NonNull
|
||||||
Connection connection;
|
Connection connection;
|
||||||
Map<String, User> users;
|
Map<String, User> users;
|
||||||
Map<String, Group> groups;
|
Map<String, Group> groups;
|
||||||
@@ -28,6 +30,13 @@ public class Configuration {
|
|||||||
Authentication authentication;
|
Authentication authentication;
|
||||||
Tls tls;
|
Tls tls;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public static class RateLimiter {
|
||||||
|
boolean delayRequest;
|
||||||
|
int messageBufferSize;
|
||||||
|
int maxQueuedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
public static class EventExecutor {
|
public static class EventExecutor {
|
||||||
boolean useVirtualThreads;
|
boolean useVirtualThreads;
|
||||||
@@ -35,12 +44,11 @@ public class Configuration {
|
|||||||
|
|
||||||
@Value
|
@Value
|
||||||
public static class Connection {
|
public static class Connection {
|
||||||
Duration readTimeout;
|
|
||||||
Duration writeTimeout;
|
|
||||||
Duration idleTimeout;
|
Duration idleTimeout;
|
||||||
Duration readIdleTimeout;
|
Duration readIdleTimeout;
|
||||||
Duration writeIdleTimeout;
|
Duration writeIdleTimeout;
|
||||||
int maxRequestSize;
|
int maxRequestSize;
|
||||||
|
int chunkSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@@ -85,17 +93,6 @@ public class Configuration {
|
|||||||
Group extract(X509Certificate cert);
|
Group extract(X509Certificate cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
|
||||||
public static class Throttling {
|
|
||||||
KeyStore keyStore;
|
|
||||||
TrustStore trustStore;
|
|
||||||
boolean verifyClients;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ClientCertificate {
|
|
||||||
REQUIRED, OPTIONAL
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
public static class Tls {
|
public static class Tls {
|
||||||
KeyStore keyStore;
|
KeyStore keyStore;
|
||||||
@@ -146,6 +143,7 @@ public class Configuration {
|
|||||||
int incomingConnectionsBacklogSize,
|
int incomingConnectionsBacklogSize,
|
||||||
String serverPath,
|
String serverPath,
|
||||||
EventExecutor eventExecutor,
|
EventExecutor eventExecutor,
|
||||||
|
RateLimiter rateLimiter,
|
||||||
Connection connection,
|
Connection connection,
|
||||||
Map<String, User> users,
|
Map<String, User> users,
|
||||||
Map<String, Group> groups,
|
Map<String, Group> groups,
|
||||||
@@ -159,6 +157,7 @@ public class Configuration {
|
|||||||
incomingConnectionsBacklogSize,
|
incomingConnectionsBacklogSize,
|
||||||
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
|
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
|
||||||
eventExecutor,
|
eventExecutor,
|
||||||
|
rateLimiter,
|
||||||
connection,
|
connection,
|
||||||
users,
|
users,
|
||||||
groups,
|
groups,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
package net.woggioni.rbcs.api;
|
package net.woggioni.rbcs.api;
|
||||||
|
|
||||||
public enum Role {
|
public enum Role {
|
||||||
Reader, Writer
|
Reader, Writer, Healthcheck
|
||||||
}
|
}
|
@@ -14,17 +14,26 @@ public sealed interface CacheMessage {
|
|||||||
private final String key;
|
private final String key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
abstract sealed class CacheGetResponse implements CacheMessage {
|
abstract sealed class CacheGetResponse implements CacheMessage {
|
||||||
|
private final String key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
|
||||||
final class CacheValueFoundResponse extends CacheGetResponse {
|
final class CacheValueFoundResponse extends CacheGetResponse {
|
||||||
private final String key;
|
|
||||||
private final CacheValueMetadata metadata;
|
private final CacheValueMetadata metadata;
|
||||||
|
|
||||||
|
public CacheValueFoundResponse(String key, CacheValueMetadata metadata) {
|
||||||
|
super(key);
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CacheValueNotFoundResponse extends CacheGetResponse {
|
final class CacheValueNotFoundResponse extends CacheGetResponse {
|
||||||
|
public CacheValueNotFoundResponse(String key) {
|
||||||
|
super(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@@ -9,39 +9,48 @@ plugins {
|
|||||||
id 'maven-publish'
|
id 'maven-publish'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
import net.woggioni.gradle.envelope.EnvelopeJarTask
|
import net.woggioni.gradle.envelope.EnvelopeJarTask
|
||||||
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
|
import net.woggioni.gradle.envelope.EnvelopePlugin
|
||||||
import net.woggioni.gradle.graalvm.NativeImagePlugin
|
import net.woggioni.gradle.graalvm.*
|
||||||
import net.woggioni.gradle.graalvm.NativeImageTask
|
|
||||||
import net.woggioni.gradle.graalvm.JlinkPlugin
|
|
||||||
import net.woggioni.gradle.graalvm.JlinkTask
|
|
||||||
|
|
||||||
Property<String> mainModuleName = objects.property(String.class)
|
sourceSets {
|
||||||
mainModuleName.set('net.woggioni.rbcs.cli')
|
configureNativeImage {
|
||||||
Property<String> mainClassName = objects.property(String.class)
|
java {
|
||||||
mainClassName.set('net.woggioni.rbcs.cli.RemoteBuildCacheServerCli')
|
}
|
||||||
|
kotlin {
|
||||||
|
|
||||||
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
|
}
|
||||||
options.javaModuleMainClass = mainClassName
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
|
||||||
release {
|
release {
|
||||||
transitive = false
|
transitive = false
|
||||||
canBeConsumed = true
|
canBeConsumed = true
|
||||||
canBeResolved = true
|
canBeResolved = true
|
||||||
visible = true
|
visible = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
envelopeJar {
|
configureNativeImageImplementation {
|
||||||
mainModule = mainModuleName
|
extendsFrom implementation
|
||||||
mainClass = mainClassName
|
}
|
||||||
|
|
||||||
|
configureNativeImageRuntimeOnly {
|
||||||
|
extendsFrom runtimeOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeImage {
|
||||||
|
extendsFrom runtimeClasspath
|
||||||
|
}
|
||||||
|
|
||||||
extraClasspath = ["plugins"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
configureNativeImageImplementation project
|
||||||
|
configureNativeImageImplementation project(':rbcs-server-memcache')
|
||||||
|
|
||||||
implementation catalog.jwo
|
implementation catalog.jwo
|
||||||
implementation catalog.slf4j.api
|
implementation catalog.slf4j.api
|
||||||
implementation catalog.picocli
|
implementation catalog.picocli
|
||||||
@@ -52,49 +61,119 @@ dependencies {
|
|||||||
// runtimeOnly catalog.slf4j.jdk14
|
// runtimeOnly catalog.slf4j.jdk14
|
||||||
runtimeOnly catalog.logback.classic
|
runtimeOnly catalog.logback.classic
|
||||||
// runtimeOnly catalog.slf4j.simple
|
// runtimeOnly catalog.slf4j.simple
|
||||||
|
nativeImage project(':rbcs-server-memcache')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
|
|
||||||
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.rbcs.LoggingConfig'
|
Property<String> mainModuleName = objects.property(String.class)
|
||||||
// systemProperties['log.config.source'] = 'net/woggioni/rbcs/cli/logging.properties'
|
mainModuleName.set('net.woggioni.rbcs.cli')
|
||||||
// systemProperties['java.util.logging.config.file'] = 'classpath:net/woggioni/rbcs/cli/logging.properties'
|
Property<String> mainClassName = objects.property(String.class)
|
||||||
|
mainClassName.set('net.woggioni.rbcs.cli.RemoteBuildCacheServerCli')
|
||||||
|
|
||||||
|
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
|
||||||
|
options.javaModuleMainClass = mainClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
Provider<Jar> jarTaskProvider = tasks.named(JavaPlugin.JAR_TASK_NAME, Jar)
|
||||||
|
|
||||||
|
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.ENVELOPE_JAR_TASK_NAME, EnvelopeJarTask.class) {
|
||||||
|
mainModule = mainModuleName
|
||||||
|
mainClass = mainClassName
|
||||||
|
|
||||||
|
extraClasspath = ["plugins"]
|
||||||
|
|
||||||
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/rbcs/cli/logback.xml'
|
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/rbcs/cli/logback.xml'
|
||||||
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED'
|
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED'
|
||||||
|
|
||||||
// systemProperties['org.slf4j.simpleLogger.showDateTime'] = 'true'
|
|
||||||
// systemProperties['org.slf4j.simpleLogger.defaultLogLevel'] = 'debug'
|
|
||||||
// systemProperties['org.slf4j.simpleLogger.log.com.google.code.yanf4j'] = 'warn'
|
|
||||||
// systemProperties['org.slf4j.simpleLogger.log.net.rubyeye.xmemcached'] = 'warn'
|
|
||||||
// systemProperties['org.slf4j.simpleLogger.dateTimeFormat'] = 'yyyy-MM-dd\'T\'HH:mm:ss.SSSZ'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
|
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
|
||||||
mainClass = mainClassName
|
toolchain {
|
||||||
mainModule = mainModuleName
|
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
|
mainClass = mainClassName
|
||||||
mainModule = mainModuleName
|
// mainModule = mainModuleName
|
||||||
useMusl = true
|
useMusl = true
|
||||||
buildStaticImage = 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
|
mainClass = mainClassName
|
||||||
mainModule = 'net.woggioni.rbcs.cli'
|
mainModule = 'net.woggioni.rbcs.cli'
|
||||||
|
classpath = project.files(
|
||||||
|
configurations.configureNativeImageRuntimeClasspath,
|
||||||
|
sourceSets.configureNativeImage.output
|
||||||
|
)
|
||||||
|
additionalModules = [
|
||||||
|
'net.woggioni.rbcs.server.memcache',
|
||||||
|
'ch.qos.logback.classic',
|
||||||
|
'jdk.crypto.ec'
|
||||||
|
]
|
||||||
|
compressionLevel = 2
|
||||||
|
stripDebug = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Provider<Tar> jlinkDistTarTaskProvider = tasks.named(JlinkPlugin.JLINK_DIST_TAR_TASK_NAME, Tar) {
|
||||||
|
exclude 'lib/libjvmcicompiler.so'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) {
|
||||||
|
from(rootProject.file('conf')) {
|
||||||
|
into('net/woggioni/rbcs/cli')
|
||||||
|
include 'logback.xml'
|
||||||
|
include 'logging.properties'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
artifacts {
|
artifacts {
|
||||||
release(envelopeJarTaskProvider)
|
release(envelopeJarTaskProvider)
|
||||||
|
release(upxTaskProvider)
|
||||||
|
release(jlinkDistTarTaskProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
publications {
|
publications {
|
||||||
maven(MavenPublication) {
|
maven(MavenPublication) {
|
||||||
artifact envelopeJar
|
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
|
#-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.PutCommand
|
||||||
import net.woggioni.rbcs.cli.impl.commands.ServerCommand
|
import net.woggioni.rbcs.cli.impl.commands.ServerCommand
|
||||||
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
|
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
|
||||||
import net.woggioni.rbcs.common.contextLogger
|
import net.woggioni.rbcs.common.createLogger
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import picocli.CommandLine.Model.CommandSpec
|
import picocli.CommandLine.Model.CommandSpec
|
||||||
|
|
||||||
@@ -23,15 +23,20 @@ class RemoteBuildCacheServerCli : RbcsCommand() {
|
|||||||
|
|
||||||
class VersionProvider : AbstractVersionProvider()
|
class VersionProvider : AbstractVersionProvider()
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
private fun setPropertyIfNotPresent(key: String, value: String) {
|
||||||
fun main(vararg args: 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
|
val currentClassLoader = RemoteBuildCacheServerCli::class.java.classLoader
|
||||||
Thread.currentThread().contextClassLoader = currentClassLoader
|
Thread.currentThread().contextClassLoader = currentClassLoader
|
||||||
if(currentClassLoader.javaClass.name == "net.woggioni.envelope.loader.ModuleClassLoader") {
|
if(currentClassLoader.javaClass.name == "net.woggioni.envelope.loader.ModuleClassLoader") {
|
||||||
//We're running in an envelope jar and custom URL protocols won't work
|
//We're running in an envelope jar and custom URL protocols won't work
|
||||||
RbcsUrlStreamHandlerFactory.install()
|
RbcsUrlStreamHandlerFactory.install()
|
||||||
}
|
}
|
||||||
val log = contextLogger()
|
val log = createLogger<RemoteBuildCacheServerCli>()
|
||||||
val app = Application.builder("rbcs")
|
val app = Application.builder("rbcs")
|
||||||
.configurationDirectoryEnvVar("RBCS_CONFIGURATION_DIR")
|
.configurationDirectoryEnvVar("RBCS_CONFIGURATION_DIR")
|
||||||
.configurationDirectoryPropertyKey("net.woggioni.rbcs.conf.dir")
|
.configurationDirectoryPropertyKey("net.woggioni.rbcs.conf.dir")
|
||||||
@@ -51,7 +56,12 @@ class RemoteBuildCacheServerCli : RbcsCommand() {
|
|||||||
addSubcommand(GetCommand())
|
addSubcommand(GetCommand())
|
||||||
addSubcommand(HealthCheckCommand())
|
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
|
package net.woggioni.rbcs.cli.impl
|
||||||
|
|
||||||
import picocli.CommandLine
|
|
||||||
import java.util.jar.Attributes
|
import java.util.jar.Attributes
|
||||||
import java.util.jar.JarFile
|
import java.util.jar.JarFile
|
||||||
import java.util.jar.Manifest
|
import java.util.jar.Manifest
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
|
|
||||||
abstract class AbstractVersionProvider : CommandLine.IVersionProvider {
|
abstract class AbstractVersionProvider : CommandLine.IVersionProvider {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package net.woggioni.rbcs.cli.impl
|
package net.woggioni.rbcs.cli.impl
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
import net.woggioni.jwo.Application
|
import net.woggioni.jwo.Application
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
|
|
||||||
abstract class RbcsCommand : Runnable {
|
abstract class RbcsCommand : Runnable {
|
||||||
@@ -12,7 +12,7 @@ abstract class RbcsCommand : Runnable {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
protected fun findConfigurationFile(app: Application, fileName : String): Path {
|
protected fun findConfigurationFile(app: Application, fileName : String): Path {
|
||||||
val confDir = app.computeConfigurationDirectory()
|
val confDir = app.computeConfigurationDirectory(false)
|
||||||
val configurationFile = confDir.resolve(fileName)
|
val configurationFile = confDir.resolve(fileName)
|
||||||
return configurationFile
|
return configurationFile
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,5 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.commands
|
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.security.SecureRandom
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -19,6 +8,18 @@ import java.util.concurrent.LinkedBlockingQueue
|
|||||||
import java.util.concurrent.Semaphore
|
import java.util.concurrent.Semaphore
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import kotlin.random.Random
|
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(
|
@CommandLine.Command(
|
||||||
name = "benchmark",
|
name = "benchmark",
|
||||||
@@ -26,8 +27,136 @@ import kotlin.random.Random
|
|||||||
showDefaultValues = true
|
showDefaultValues = true
|
||||||
)
|
)
|
||||||
class BenchmarkCommand : RbcsCommand() {
|
class BenchmarkCommand : RbcsCommand() {
|
||||||
companion object{
|
companion object {
|
||||||
private val log = createLogger<BenchmarkCommand>()
|
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
|
@CommandLine.Spec
|
||||||
@@ -60,113 +189,11 @@ class BenchmarkCommand : RbcsCommand() {
|
|||||||
clientCommand.configuration.profiles[profileName]
|
clientCommand.configuration.profiles[profileName]
|
||||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||||
}
|
}
|
||||||
val progressThreshold = LongMath.ceilDiv(numberOfEntries.toLong(), 20)
|
execute(
|
||||||
RemoteBuildCacheClient(profile).use { client ->
|
profile,
|
||||||
|
numberOfEntries,
|
||||||
val entryGenerator = sequence {
|
size,
|
||||||
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
|
randomValues
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,10 +1,12 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.commands
|
package net.woggioni.rbcs.cli.impl.commands
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
import net.woggioni.jwo.Application
|
import net.woggioni.jwo.Application
|
||||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
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 picocli.CommandLine
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
name = "client",
|
name = "client",
|
||||||
@@ -24,15 +26,20 @@ class ClientCommand(app : Application) : RbcsCommand() {
|
|||||||
names = ["-p", "--profile"],
|
names = ["-p", "--profile"],
|
||||||
description = ["Name of the client profile to be used"],
|
description = ["Name of the client profile to be used"],
|
||||||
paramLabel = "PROFILE",
|
paramLabel = "PROFILE",
|
||||||
required = true
|
required = false
|
||||||
)
|
)
|
||||||
var profileName : String? = null
|
var profileName : String? = null
|
||||||
|
get() = field ?: throw IllegalArgumentException("A profile name must be specified using the '-p' command line parameter")
|
||||||
|
|
||||||
val configuration : RemoteBuildCacheClient.Configuration by lazy {
|
val configuration : Configuration by lazy {
|
||||||
RemoteBuildCacheClient.Configuration.parse(configurationFile)
|
Configuration.parse(configurationFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
|
val log = createLogger<ClientCommand>()
|
||||||
|
log.debug {
|
||||||
|
"Using configuration file '$configurationFile'"
|
||||||
|
}
|
||||||
println("Available profiles:")
|
println("Available profiles:")
|
||||||
configuration.profiles.forEach { (profileName, _) ->
|
configuration.profiles.forEach { (profileName, _) ->
|
||||||
println(profileName)
|
println(profileName)
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.commands
|
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.cli.impl.RbcsCommand
|
||||||
|
import net.woggioni.rbcs.client.Configuration
|
||||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||||
import net.woggioni.rbcs.common.createLogger
|
import net.woggioni.rbcs.common.createLogger
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
name = "get",
|
name = "get",
|
||||||
@@ -13,8 +15,20 @@ import java.nio.file.Path
|
|||||||
showDefaultValues = true
|
showDefaultValues = true
|
||||||
)
|
)
|
||||||
class GetCommand : RbcsCommand() {
|
class GetCommand : RbcsCommand() {
|
||||||
companion object{
|
companion object {
|
||||||
private val log = createLogger<GetCommand>()
|
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
|
@CommandLine.Spec
|
||||||
@@ -40,14 +54,6 @@ class GetCommand : RbcsCommand() {
|
|||||||
clientCommand.configuration.profiles[profileName]
|
clientCommand.configuration.profiles[profileName]
|
||||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||||
}
|
}
|
||||||
RemoteBuildCacheClient(profile).use { client ->
|
execute(profile, key, (output?.let(Files::newOutputStream) ?: System.out))
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,11 +1,12 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.commands
|
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.cli.impl.RbcsCommand
|
||||||
|
import net.woggioni.rbcs.client.Configuration
|
||||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||||
import net.woggioni.rbcs.common.createLogger
|
import net.woggioni.rbcs.common.createLogger
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.security.SecureRandom
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
name = "health",
|
name = "health",
|
||||||
@@ -15,6 +16,27 @@ import kotlin.random.Random
|
|||||||
class HealthCheckCommand : RbcsCommand() {
|
class HealthCheckCommand : RbcsCommand() {
|
||||||
companion object{
|
companion object{
|
||||||
private val log = createLogger<HealthCheckCommand>()
|
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
|
@CommandLine.Spec
|
||||||
@@ -26,23 +48,6 @@ class HealthCheckCommand : RbcsCommand() {
|
|||||||
clientCommand.configuration.profiles[profileName]
|
clientCommand.configuration.profiles[profileName]
|
||||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||||
}
|
}
|
||||||
RemoteBuildCacheClient(profile).use { client ->
|
execute(profile)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,13 +1,13 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.commands
|
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.jwo.UncloseableOutputStream
|
||||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||||
import net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter
|
import net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter
|
||||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.io.OutputStream
|
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
import java.io.PrintWriter
|
|
||||||
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
|
@@ -1,17 +1,18 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.commands
|
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.Hash
|
||||||
import net.woggioni.jwo.JWO
|
import net.woggioni.jwo.JWO
|
||||||
import net.woggioni.jwo.NullOutputStream
|
import net.woggioni.jwo.NullOutputStream
|
||||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||||
|
import net.woggioni.rbcs.client.Configuration
|
||||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||||
import net.woggioni.rbcs.common.createLogger
|
import net.woggioni.rbcs.common.createLogger
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.io.InputStream
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
name = "put",
|
name = "put",
|
||||||
@@ -19,8 +20,22 @@ import java.util.UUID
|
|||||||
showDefaultValues = true
|
showDefaultValues = true
|
||||||
)
|
)
|
||||||
class PutCommand : RbcsCommand() {
|
class PutCommand : RbcsCommand() {
|
||||||
companion object{
|
companion object {
|
||||||
private val log = createLogger<PutCommand>()
|
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()
|
actualKey = key ?: UUID.randomUUID().toString()
|
||||||
}
|
}
|
||||||
inputStream.use {
|
execute(profile, actualKey, inputStream, mimeType, contentDisposition)
|
||||||
client.put(actualKey, it.readAllBytes(), CacheValueMetadata(contentDisposition, mimeType))
|
|
||||||
}.get()
|
|
||||||
println(profile.serverURI.resolve(actualKey))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,10 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.commands
|
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.Application
|
||||||
import net.woggioni.jwo.JWO
|
import net.woggioni.jwo.JWO
|
||||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
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
|
||||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
|
import net.woggioni.rbcs.server.RemoteBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
|
||||||
import picocli.CommandLine
|
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(
|
@CommandLine.Command(
|
||||||
name = "server",
|
name = "server",
|
||||||
@@ -59,6 +59,9 @@ class ServerCommand(app : Application) : RbcsCommand() {
|
|||||||
createDefaultConfigurationFile(configurationFile)
|
createDefaultConfigurationFile(configurationFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug {
|
||||||
|
"Using configuration file '$configurationFile'"
|
||||||
|
}
|
||||||
val configuration = RemoteBuildCacheServer.loadConfiguration(configurationFile)
|
val configuration = RemoteBuildCacheServer.loadConfiguration(configurationFile)
|
||||||
log.debug {
|
log.debug {
|
||||||
ByteArrayOutputStream().also {
|
ByteArrayOutputStream().also {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.converters
|
package net.woggioni.rbcs.cli.impl.converters
|
||||||
|
|
||||||
import picocli.CommandLine
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
|
|
||||||
class DurationConverter : CommandLine.ITypeConverter<Duration> {
|
class DurationConverter : CommandLine.ITypeConverter<Duration> {
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.converters
|
package net.woggioni.rbcs.cli.impl.converters
|
||||||
|
|
||||||
import picocli.CommandLine
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
|
|
||||||
class InputStreamConverter : CommandLine.ITypeConverter<InputStream> {
|
class InputStreamConverter : CommandLine.ITypeConverter<InputStream> {
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
package net.woggioni.rbcs.cli.impl.converters
|
package net.woggioni.rbcs.cli.impl.converters
|
||||||
|
|
||||||
import picocli.CommandLine
|
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
|
|
||||||
class OutputStreamConverter : CommandLine.ITypeConverter<OutputStream> {
|
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.ByteBuf
|
||||||
import io.netty.buffer.Unpooled
|
import io.netty.buffer.Unpooled
|
||||||
import io.netty.channel.Channel
|
import io.netty.channel.Channel
|
||||||
import io.netty.channel.ChannelHandler
|
|
||||||
import io.netty.channel.ChannelHandlerContext
|
import io.netty.channel.ChannelHandlerContext
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
|
||||||
import io.netty.channel.ChannelOption
|
import io.netty.channel.ChannelOption
|
||||||
import io.netty.channel.ChannelPipeline
|
import io.netty.channel.ChannelPipeline
|
||||||
|
import io.netty.channel.IoEventLoopGroup
|
||||||
|
import io.netty.channel.MultiThreadIoEventLoopGroup
|
||||||
import io.netty.channel.SimpleChannelInboundHandler
|
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.AbstractChannelPoolHandler
|
||||||
import io.netty.channel.pool.ChannelPool
|
import io.netty.channel.pool.ChannelPool
|
||||||
import io.netty.channel.pool.FixedChannelPool
|
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.IdleStateEvent
|
||||||
import io.netty.handler.timeout.IdleStateHandler
|
import io.netty.handler.timeout.IdleStateHandler
|
||||||
import io.netty.util.concurrent.Future
|
import io.netty.util.concurrent.Future
|
||||||
|
import io.netty.util.concurrent.Future as NettyFuture
|
||||||
import io.netty.util.concurrent.GenericFutureListener
|
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.io.IOException
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.security.PrivateKey
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
import kotlin.random.Random
|
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 {
|
class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
|
||||||
companion object{
|
companion object {
|
||||||
private val log = createLogger<RemoteBuildCacheClient>()
|
private val log = createLogger<RemoteBuildCacheClient>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val group: NioEventLoopGroup
|
private val group: IoEventLoopGroup
|
||||||
private var sslContext: SslContext
|
private val sslContext: SslContext
|
||||||
private val pool: ChannelPool
|
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 {
|
init {
|
||||||
group = NioEventLoopGroup()
|
group = MultiThreadIoEventLoopGroup(NioIoHandler.newFactory())
|
||||||
sslContext = SslContextBuilder.forClient().also { builder ->
|
sslContext = SslContextBuilder.forClient().also { builder ->
|
||||||
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
|
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
|
||||||
builder.keyManager(
|
builder.apply {
|
||||||
tlsClientAuthenticationCredentials.key,
|
keyManager(
|
||||||
*tlsClientAuthenticationCredentials.certificateChain
|
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()
|
}.build()
|
||||||
|
|
||||||
@@ -177,20 +152,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
|||||||
}
|
}
|
||||||
val pipeline: ChannelPipeline = ch.pipeline()
|
val pipeline: ChannelPipeline = ch.pipeline()
|
||||||
|
|
||||||
profile.connection?.also { conn ->
|
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val readIdleTimeout = conn.readIdleTimeout.toMillis()
|
val readIdleTimeout = conn.readIdleTimeout.toMillis()
|
||||||
val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
|
val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
|
||||||
val idleTimeout = conn.idleTimeout.toMillis()
|
val idleTimeout = conn.idleTimeout.toMillis()
|
||||||
@@ -214,7 +176,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
|||||||
|
|
||||||
// HTTP handlers
|
// HTTP handlers
|
||||||
pipeline.addLast("codec", HttpClientCodec())
|
pipeline.addLast("codec", HttpClientCodec())
|
||||||
if(profile.compressionEnabled) {
|
if (profile.compressionEnabled) {
|
||||||
pipeline.addLast("decompressor", HttpContentDecompressor())
|
pipeline.addLast("decompressor", HttpContentDecompressor())
|
||||||
}
|
}
|
||||||
pipeline.addLast("aggregator", HttpObjectAggregator(134217728))
|
pipeline.addLast("aggregator", HttpObjectAggregator(134217728))
|
||||||
@@ -333,50 +295,33 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
|||||||
): CompletableFuture<FullHttpResponse> {
|
): CompletableFuture<FullHttpResponse> {
|
||||||
val responseFuture = CompletableFuture<FullHttpResponse>()
|
val responseFuture = CompletableFuture<FullHttpResponse>()
|
||||||
// Custom handler for processing responses
|
// Custom handler for processing responses
|
||||||
|
|
||||||
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
||||||
private val handlers = mutableListOf<ChannelHandler>()
|
|
||||||
|
|
||||||
fun cleanup(channel: Channel, pipeline: ChannelPipeline) {
|
|
||||||
handlers.forEach(pipeline::remove)
|
|
||||||
pool.release(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun operationComplete(channelFuture: Future<Channel>) {
|
override fun operationComplete(channelFuture: Future<Channel>) {
|
||||||
if (channelFuture.isSuccess) {
|
if (channelFuture.isSuccess) {
|
||||||
val channel = channelFuture.now
|
val channel = channelFuture.now
|
||||||
val pipeline = channel.pipeline()
|
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>> {
|
val closeListener = GenericFutureListener<Future<Void>> {
|
||||||
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
||||||
pool.release(channel)
|
|
||||||
}
|
}
|
||||||
|
channel.closeFuture().addListener(closeListener)
|
||||||
|
|
||||||
val responseHandler = object : SimpleChannelInboundHandler<FullHttpResponse>() {
|
val responseHandler = object : SimpleChannelInboundHandler<FullHttpResponse>() {
|
||||||
|
|
||||||
|
override fun handlerAdded(ctx: ChannelHandlerContext) {
|
||||||
|
channel.closeFuture().removeListener(closeListener)
|
||||||
|
}
|
||||||
|
|
||||||
override fun channelRead0(
|
override fun channelRead0(
|
||||||
ctx: ChannelHandlerContext,
|
ctx: ChannelHandlerContext,
|
||||||
response: FullHttpResponse
|
response: FullHttpResponse
|
||||||
) {
|
) {
|
||||||
channel.closeFuture().removeListener(closeListener)
|
pipeline.remove(this)
|
||||||
cleanup(channel, pipeline)
|
|
||||||
responseFuture.complete(response)
|
responseFuture.complete(response)
|
||||||
|
if (!profile.connection.requestPipelining) {
|
||||||
|
pool.release(channel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||||
@@ -390,16 +335,33 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun channelInactive(ctx: ChannelHandlerContext) {
|
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||||
pool.release(channel)
|
|
||||||
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
||||||
super.channelInactive(ctx)
|
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)) {
|
pipeline.addLast(responseHandler)
|
||||||
handlers.add(handler)
|
|
||||||
}
|
|
||||||
pipeline.addLast(timeoutHandler, responseHandler)
|
|
||||||
channel.closeFuture().addListener(closeListener)
|
|
||||||
|
|
||||||
|
|
||||||
// Prepare the HTTP request
|
// Prepare the HTTP request
|
||||||
@@ -411,13 +373,14 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
|||||||
uri.rawPath,
|
uri.rawPath,
|
||||||
content ?: Unpooled.buffer(0)
|
content ?: Unpooled.buffer(0)
|
||||||
).apply {
|
).apply {
|
||||||
|
// Set headers
|
||||||
headers().apply {
|
headers().apply {
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
|
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
|
||||||
}
|
}
|
||||||
set(HttpHeaderNames.HOST, profile.serverURI.host)
|
set(HttpHeaderNames.HOST, profile.serverURI.host)
|
||||||
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
|
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
|
||||||
if(profile.compressionEnabled) {
|
if (profile.compressionEnabled) {
|
||||||
set(
|
set(
|
||||||
HttpHeaderNames.ACCEPT_ENCODING,
|
HttpHeaderNames.ACCEPT_ENCODING,
|
||||||
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
|
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
|
||||||
@@ -436,9 +399,16 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set headers
|
|
||||||
// Send the request
|
// 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 {
|
} else {
|
||||||
responseFuture.completeExceptionally(channelFuture.cause())
|
responseFuture.completeExceptionally(channelFuture.cause())
|
||||||
}
|
}
|
@@ -1,10 +1,5 @@
|
|||||||
package net.woggioni.rbcs.client.impl
|
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.net.URI
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@@ -13,12 +8,17 @@ import java.security.PrivateKey
|
|||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.temporal.ChronoUnit
|
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 {
|
object Parser {
|
||||||
|
|
||||||
fun parse(document: Document): RemoteBuildCacheClient.Configuration {
|
fun parse(document: Document): Configuration {
|
||||||
val root = document.documentElement
|
val root = document.documentElement
|
||||||
val profiles = mutableMapOf<String, RemoteBuildCacheClient.Configuration.Profile>()
|
val profiles = mutableMapOf<String, Configuration.Profile>()
|
||||||
|
|
||||||
for (child in root.asIterable()) {
|
for (child in root.asIterable()) {
|
||||||
val tagName = child.localName
|
val tagName = child.localName
|
||||||
@@ -28,9 +28,15 @@ object Parser {
|
|||||||
child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
|
child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
|
||||||
val uri = child.renderAttribute("base-url")?.let(::URI)
|
val uri = child.renderAttribute("base-url")?.let(::URI)
|
||||||
?: throw ConfigurationException("base-url attribute is required")
|
?: throw ConfigurationException("base-url attribute is required")
|
||||||
var authentication: RemoteBuildCacheClient.Configuration.Authentication? = null
|
var authentication: Configuration.Authentication? = null
|
||||||
var retryPolicy: RemoteBuildCacheClient.Configuration.RetryPolicy? = null
|
var retryPolicy: Configuration.RetryPolicy? = null
|
||||||
var connection : RemoteBuildCacheClient.Configuration.Connection? = 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()) {
|
for (gchild in child.asIterable()) {
|
||||||
when (gchild.localName) {
|
when (gchild.localName) {
|
||||||
"tls-client-auth" -> {
|
"tls-client-auth" -> {
|
||||||
@@ -51,7 +57,7 @@ object Parser {
|
|||||||
.toList()
|
.toList()
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
authentication =
|
authentication =
|
||||||
RemoteBuildCacheClient.Configuration.Authentication.TlsClientAuthenticationCredentials(
|
Configuration.Authentication.TlsClientAuthenticationCredentials(
|
||||||
key,
|
key,
|
||||||
certChain
|
certChain
|
||||||
)
|
)
|
||||||
@@ -63,7 +69,7 @@ object Parser {
|
|||||||
val password = gchild.renderAttribute("password")
|
val password = gchild.renderAttribute("password")
|
||||||
?: throw ConfigurationException("password attribute is required")
|
?: throw ConfigurationException("password attribute is required")
|
||||||
authentication =
|
authentication =
|
||||||
RemoteBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials(
|
Configuration.Authentication.BasicAuthenticationCredentials(
|
||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
)
|
)
|
||||||
@@ -82,7 +88,7 @@ object Parser {
|
|||||||
gchild.renderAttribute("exp")
|
gchild.renderAttribute("exp")
|
||||||
?.let(String::toDouble)
|
?.let(String::toDouble)
|
||||||
?: 2.0f
|
?: 2.0f
|
||||||
retryPolicy = RemoteBuildCacheClient.Configuration.RetryPolicy(
|
retryPolicy = Configuration.RetryPolicy(
|
||||||
maxAttempts,
|
maxAttempts,
|
||||||
initialDelay.toMillis(),
|
initialDelay.toMillis(),
|
||||||
exp.toDouble()
|
exp.toDouble()
|
||||||
@@ -90,24 +96,32 @@ object Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"connection" -> {
|
"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")
|
val idleTimeout = gchild.renderAttribute("idle-timeout")
|
||||||
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
|
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
|
||||||
val readIdleTimeout = gchild.renderAttribute("read-idle-timeout")
|
val readIdleTimeout = gchild.renderAttribute("read-idle-timeout")
|
||||||
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
||||||
val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout")
|
val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout")
|
||||||
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
||||||
connection = RemoteBuildCacheClient.Configuration.Connection(
|
val requestPipelining = gchild.renderAttribute("request-pipelining")
|
||||||
readTimeout,
|
?.let(String::toBoolean) ?: false
|
||||||
writeTimeout,
|
connection = Configuration.Connection(
|
||||||
idleTimeout,
|
|
||||||
readIdleTimeout,
|
readIdleTimeout,
|
||||||
writeIdleTimeout,
|
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")
|
val maxConnections = child.renderAttribute("max-connections")
|
||||||
@@ -119,18 +133,19 @@ object Parser {
|
|||||||
?.let(String::toBoolean)
|
?.let(String::toBoolean)
|
||||||
?: true
|
?: true
|
||||||
|
|
||||||
profiles[name] = RemoteBuildCacheClient.Configuration.Profile(
|
profiles[name] = Configuration.Profile(
|
||||||
uri,
|
uri,
|
||||||
connection,
|
connection,
|
||||||
authentication,
|
authentication,
|
||||||
connectionTimeout,
|
connectionTimeout,
|
||||||
maxConnections,
|
maxConnections,
|
||||||
compressionEnabled,
|
compressionEnabled,
|
||||||
retryPolicy
|
retryPolicy,
|
||||||
|
trustStore
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RemoteBuildCacheClient.Configuration(profiles)
|
return Configuration(profiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -15,46 +15,246 @@
|
|||||||
<xs:complexType name="profileType">
|
<xs:complexType name="profileType">
|
||||||
<xs:sequence>
|
<xs:sequence>
|
||||||
<xs:choice>
|
<xs:choice>
|
||||||
<xs:element name="no-auth" type="rbcs-client:noAuthType"/>
|
<xs:element name="no-auth" type="rbcs-client:noAuthType">
|
||||||
<xs:element name="basic-auth" type="rbcs-client:basicAuthType"/>
|
<xs:annotation>
|
||||||
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType"/>
|
<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:choice>
|
||||||
<xs:element name="connection" type="rbcs-client:connectionType" minOccurs="0" />
|
<xs:element name="connection" type="rbcs-client:connectionType" minOccurs="0" >
|
||||||
<xs:element name="retry-policy" type="rbcs-client:retryType" 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:sequence>
|
||||||
<xs:attribute name="name" type="xs:token" use="required"/>
|
<xs:attribute name="name" type="xs:token" use="required">
|
||||||
<xs:attribute name="base-url" type="xs:anyURI" use="required"/>
|
<xs:annotation>
|
||||||
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
|
<xs:documentation>
|
||||||
<xs:attribute name="connection-timeout" type="xs:duration"/>
|
Name of this server profile, to be referred to from rbcs-cli with the '-p' parameter
|
||||||
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
|
</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>
|
||||||
|
|
||||||
<xs:complexType name="connectionType">
|
<xs:complexType name="connectionType">
|
||||||
<xs:attribute name="read-timeout" type="xs:duration" use="optional" default="PT0S"/>
|
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S">
|
||||||
<xs:attribute name="write-timeout" type="xs:duration" use="optional" default="PT0S"/>
|
<xs:annotation>
|
||||||
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S"/>
|
<xs:documentation>
|
||||||
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
|
The client will close the connection with the server
|
||||||
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
|
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>
|
||||||
|
|
||||||
<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:complexType name="basicAuthType">
|
||||||
<xs:attribute name="user" type="xs:token" use="required"/>
|
<xs:annotation>
|
||||||
<xs:attribute name="password" type="xs:string" use="required"/>
|
<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>
|
||||||
|
|
||||||
<xs:complexType name="tlsClientAuthType">
|
<xs:complexType name="tlsClientAuthType">
|
||||||
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/>
|
<xs:attribute name="key-store-file" type="xs:anyURI" use="required">
|
||||||
<xs:attribute name="key-store-password" type="xs:string" use="required"/>
|
<xs:annotation>
|
||||||
<xs:attribute name="key-alias" type="xs:token" use="required"/>
|
<xs:documentation>
|
||||||
<xs:attribute name="key-password" type="xs:string" use="optional"/>
|
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>
|
||||||
|
|
||||||
<xs:complexType name="retryType">
|
<xs:complexType name="retryType">
|
||||||
<xs:attribute name="max-attempts" type="xs:positiveInteger" use="required"/>
|
<xs:annotation>
|
||||||
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S"/>
|
<xs:documentation>
|
||||||
<xs:attribute name="exp" type="xs:double" default="2.0"/>
|
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>
|
||||||
|
|
||||||
|
<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>
|
</xs:schema>
|
||||||
|
@@ -2,6 +2,9 @@ package net.woggioni.rbcs.client
|
|||||||
|
|
||||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||||
import io.netty.util.concurrent.EventExecutorGroup
|
import io.netty.util.concurrent.EventExecutorGroup
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.stream.Stream
|
||||||
|
import kotlin.random.Random
|
||||||
import net.woggioni.rbcs.common.contextLogger
|
import net.woggioni.rbcs.common.contextLogger
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
import org.junit.jupiter.api.extension.ExtensionContext
|
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.Arguments
|
||||||
import org.junit.jupiter.params.provider.ArgumentsProvider
|
import org.junit.jupiter.params.provider.ArgumentsProvider
|
||||||
import org.junit.jupiter.params.provider.ArgumentsSource
|
import org.junit.jupiter.params.provider.ArgumentsSource
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.stream.Stream
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class RetryTest {
|
class RetryTest {
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ class RetryTest {
|
|||||||
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
|
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
|
||||||
val actualTimestamp = timestamp
|
val actualTimestamp = timestamp
|
||||||
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
|
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
|
||||||
Assertions.assertTrue(err < 1e-2)
|
Assertions.assertTrue(err < 0.1)
|
||||||
}
|
}
|
||||||
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
|
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
|
||||||
/*
|
/*
|
||||||
|
@@ -9,6 +9,8 @@
|
|||||||
key-store-password="password"
|
key-store-password="password"
|
||||||
key-alias="woggioni@c962475fa38"
|
key-alias="woggioni@c962475fa38"
|
||||||
key-password="key-password"/>
|
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>
|
||||||
<profile name="profile2" base-url="https://rbcs2.example.com/">
|
<profile name="profile2" base-url="https://rbcs2.example.com/">
|
||||||
<basic-auth user="user" password="password"/>
|
<basic-auth user="user" password="password"/>
|
||||||
|
@@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':rbcs-api')
|
implementation catalog.netty.transport
|
||||||
implementation catalog.slf4j.api
|
implementation catalog.slf4j.api
|
||||||
implementation catalog.jwo
|
implementation catalog.jwo
|
||||||
implementation catalog.netty.buffer
|
implementation catalog.netty.buffer
|
||||||
|
@@ -2,14 +2,14 @@ package net.woggioni.rbcs.common
|
|||||||
|
|
||||||
import io.netty.channel.Channel
|
import io.netty.channel.Channel
|
||||||
import io.netty.channel.ChannelHandlerContext
|
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.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.slf4j.MDC
|
import org.slf4j.MDC
|
||||||
import org.slf4j.event.Level
|
import org.slf4j.event.Level
|
||||||
import org.slf4j.spi.LoggingEventBuilder
|
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> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
|
||||||
inline fun <reified T> createLogger() = LoggerFactory.getLogger(T::class.java)
|
inline fun <reified T> createLogger() = LoggerFactory.getLogger(T::class.java)
|
||||||
|
@@ -7,7 +7,18 @@ import javax.crypto.SecretKeyFactory
|
|||||||
import javax.crypto.spec.PBEKeySpec
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
|
||||||
object PasswordSecurity {
|
object PasswordSecurity {
|
||||||
private const val KEY_LENGTH = 256
|
|
||||||
|
enum class Algorithm(
|
||||||
|
val codeName : String,
|
||||||
|
val keyLength : Int,
|
||||||
|
val iterations : Int) {
|
||||||
|
PBEWithHmacSHA512_224AndAES_256("PBEWithHmacSHA512/224AndAES_256", 64, 1),
|
||||||
|
PBEWithHmacSHA1AndAES_256("PBEWithHmacSHA1AndAES_256",64, 1),
|
||||||
|
PBEWithHmacSHA384AndAES_128("PBEWithHmacSHA384AndAES_128", 64,1),
|
||||||
|
PBEWithHmacSHA384AndAES_256("PBEWithHmacSHA384AndAES_256",64,1),
|
||||||
|
PBKDF2WithHmacSHA512("PBKDF2WithHmacSHA512",512, 1),
|
||||||
|
PBKDF2WithHmacSHA384("PBKDF2WithHmacSHA384",384, 1);
|
||||||
|
}
|
||||||
|
|
||||||
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
|
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
|
||||||
val result = ByteArray(arr1.size + arr2.size)
|
val result = ByteArray(arr1.size + arr2.size)
|
||||||
@@ -23,22 +34,22 @@ object PasswordSecurity {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hashPassword(password : String, salt : String? = null) : String {
|
fun hashPassword(password : String, salt : String? = null, algorithm : Algorithm = Algorithm.PBKDF2WithHmacSHA512) : String {
|
||||||
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
|
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
|
||||||
val result = ByteArray(16)
|
val result = ByteArray(16)
|
||||||
nextBytes(result)
|
nextBytes(result)
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, 10, KEY_LENGTH)
|
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, algorithm.iterations, algorithm.keyLength)
|
||||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
val factory = SecretKeyFactory.getInstance(algorithm.codeName)
|
||||||
val hash = factory.generateSecret(spec).encoded
|
val hash = factory.generateSecret(spec).encoded
|
||||||
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
|
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> {
|
fun decodePasswordHash(encodedPasswordHash : String, algorithm: Algorithm = Algorithm.PBKDF2WithHmacSHA512) : Pair<ByteArray, ByteArray> {
|
||||||
val decoded = Base64.getDecoder().decode(passwordHash)
|
val decoded = Base64.getDecoder().decode(encodedPasswordHash)
|
||||||
val hash = ByteArray(KEY_LENGTH / 8)
|
val hash = ByteArray(algorithm.keyLength / 8)
|
||||||
val salt = ByteArray(decoded.size - KEY_LENGTH / 8)
|
val salt = ByteArray(decoded.size - algorithm.keyLength / 8)
|
||||||
System.arraycopy(decoded, 0, hash, 0, hash.size)
|
System.arraycopy(decoded, 0, hash, 0, hash.size)
|
||||||
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
|
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
|
||||||
return hash to salt
|
return hash to salt
|
||||||
|
@@ -1,29 +1,46 @@
|
|||||||
package net.woggioni.rbcs.common
|
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.URI
|
||||||
import java.net.URL
|
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.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 {
|
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_NAMESPACE_URI: String = "urn:net.woggioni.rbcs.server"
|
||||||
const val RBCS_PREFIX: String = "rbcs"
|
const val RBCS_PREFIX: String = "rbcs"
|
||||||
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
|
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
|
||||||
fun ByteArray.toInt(index : Int = 0) : Long {
|
fun ByteArray.toInt(index: Int = 0): Long {
|
||||||
if(index + 4 > size) throw IllegalArgumentException("Not enough bytes to decode a 32 bits integer")
|
if (index + 4 > size) throw IllegalArgumentException("Not enough bytes to decode a 32 bits integer")
|
||||||
var value : Long = 0
|
var value: Long = 0
|
||||||
for (b in index until index + 4) {
|
for (b in index until index + 4) {
|
||||||
value = (value shl 8) + (get(b).toInt() and 0xFF)
|
value = (value shl 8) + (get(b).toInt() and 0xFF)
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ByteArray.toLong(index : Int = 0) : Long {
|
fun ByteArray.toLong(index: Int = 0): Long {
|
||||||
if(index + 8 > size) throw IllegalArgumentException("Not enough bytes to decode a 64 bits long integer")
|
if (index + 8 > size) throw IllegalArgumentException("Not enough bytes to decode a 64 bits long integer")
|
||||||
var value : Long = 0
|
var value: Long = 0
|
||||||
for (b in index until index + 8) {
|
for (b in index until index + 8) {
|
||||||
value = (value shl 8) + (get(b).toInt() and 0xFF)
|
value = (value shl 8) + (get(b).toInt() and 0xFF)
|
||||||
}
|
}
|
||||||
@@ -32,7 +49,7 @@ object RBCS {
|
|||||||
|
|
||||||
fun digest(
|
fun digest(
|
||||||
data: ByteArray,
|
data: ByteArray,
|
||||||
md: MessageDigest = MessageDigest.getInstance("MD5")
|
md: MessageDigest
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
md.update(data)
|
md.update(data)
|
||||||
return md.digest()
|
return md.digest()
|
||||||
@@ -40,16 +57,23 @@ object RBCS {
|
|||||||
|
|
||||||
fun digestString(
|
fun digestString(
|
||||||
data: ByteArray,
|
data: ByteArray,
|
||||||
md: MessageDigest = MessageDigest.getInstance("MD5")
|
md: MessageDigest
|
||||||
): String {
|
): String {
|
||||||
return JWO.bytesToHex(digest(data, md))
|
return JWO.bytesToHex(digest(data, md))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processCacheKey(key: String, digestAlgorithm: String?) = digestAlgorithm
|
fun processCacheKey(key: String, keyPrefix: String?, digestAlgorithm: String?) : ByteArray {
|
||||||
?.let(MessageDigest::getInstance)
|
val prefixedKey = if (keyPrefix == null) {
|
||||||
?.let { md ->
|
key
|
||||||
digest(key.toByteArray(), md)
|
} else {
|
||||||
} ?: key.toByteArray(Charsets.UTF_8)
|
key + keyPrefix
|
||||||
|
}.toByteArray(Charsets.UTF_8)
|
||||||
|
return digestAlgorithm
|
||||||
|
?.let(MessageDigest::getInstance)
|
||||||
|
?.let { md ->
|
||||||
|
digest(prefixedKey, md)
|
||||||
|
} ?: prefixedKey
|
||||||
|
}
|
||||||
|
|
||||||
fun Long.toIntOrNull(): Int? {
|
fun Long.toIntOrNull(): Int? {
|
||||||
return if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) {
|
return if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) {
|
||||||
@@ -58,4 +82,86 @@ object RBCS {
|
|||||||
null
|
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
|
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.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@@ -25,7 +16,16 @@ import javax.xml.transform.stream.StreamResult
|
|||||||
import javax.xml.transform.stream.StreamSource
|
import javax.xml.transform.stream.StreamSource
|
||||||
import javax.xml.validation.Schema
|
import javax.xml.validation.Schema
|
||||||
import javax.xml.validation.SchemaFactory
|
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.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> {
|
class NodeListIterator(private val nodeList: NodeList) : Iterator<Node> {
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordHashingTest {
|
||||||
|
|
||||||
|
@EnumSource(PasswordSecurity.Algorithm::class)
|
||||||
|
@ParameterizedTest
|
||||||
|
fun test(algo: PasswordSecurity.Algorithm) {
|
||||||
|
val password = "password"
|
||||||
|
val encoded = hashPassword(password, algorithm = algo)
|
||||||
|
val (_, salt) = decodePasswordHash(encoded, algo)
|
||||||
|
Assertions.assertEquals(encoded,
|
||||||
|
hashPassword(password, salt = salt.let(Base64.getEncoder()::encodeToString), algorithm = algo)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun listAvailableAlgorithms() {
|
||||||
|
Security.getProviders().asSequence()
|
||||||
|
.flatMap { provider: Provider -> provider.services.asSequence() }
|
||||||
|
.filter { service: Provider.Service -> "SecretKeyFactory" == service.type }
|
||||||
|
.map(Provider.Service::getAlgorithm)
|
||||||
|
.forEach {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
45
rbcs-server-memcache/README.md
Normal file
45
rbcs-server-memcache/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# RBCS Memcache plugins
|
||||||
|
|
||||||
|
This plugins allows RBCs to store and retrieve data from a memcache cluster.
|
||||||
|
The memcache server selection is simply based on the hash of the key,
|
||||||
|
deflate compression is also supported and performed by the RBCS server
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
The plugin can be built with
|
||||||
|
```bash
|
||||||
|
./gradlew rbcs-server-memcache:bundle
|
||||||
|
```
|
||||||
|
which creates a `.tar` archive in the `build/distributions` folder.
|
||||||
|
The archive is supposed to be extracted inside the RBCS server's `plugins` directory.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The plugin can be enabled setting the `xs:type` attribute of the `cache` element
|
||||||
|
to `memcacheCacheType`.
|
||||||
|
|
||||||
|
The plugins currently supports the following configuration attributes:
|
||||||
|
- `max-age`: the amount of time cache entries will be retained on memcache
|
||||||
|
- `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 (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
|
||||||
|
<?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"
|
||||||
|
>
|
||||||
|
...
|
||||||
|
<cache xs:type="rbcs-memcache:memcacheCacheType"
|
||||||
|
max-age="P7D"
|
||||||
|
digest="SHA-256"
|
||||||
|
compression-mode="deflate"
|
||||||
|
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,20 +1,35 @@
|
|||||||
package net.woggioni.rbcs.server.memcache
|
package net.woggioni.rbcs.server.memcache
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelFactory
|
||||||
|
import io.netty.channel.EventLoopGroup
|
||||||
|
import io.netty.channel.pool.FixedChannelPool
|
||||||
|
import io.netty.channel.socket.DatagramChannel
|
||||||
|
import io.netty.channel.socket.SocketChannel
|
||||||
|
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.CacheHandlerFactory
|
||||||
import net.woggioni.rbcs.api.Configuration
|
import net.woggioni.rbcs.api.Configuration
|
||||||
import net.woggioni.rbcs.common.HostAndPort
|
import net.woggioni.rbcs.common.HostAndPort
|
||||||
|
import net.woggioni.rbcs.common.createLogger
|
||||||
import net.woggioni.rbcs.server.memcache.client.MemcacheClient
|
import net.woggioni.rbcs.server.memcache.client.MemcacheClient
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
data class MemcacheCacheConfiguration(
|
data class MemcacheCacheConfiguration(
|
||||||
val servers: List<Server>,
|
val servers: List<Server>,
|
||||||
val maxAge: Duration = Duration.ofDays(1),
|
val maxAge: Duration = Duration.ofDays(1),
|
||||||
|
val keyPrefix : String? = null,
|
||||||
val digestAlgorithm: String? = null,
|
val digestAlgorithm: String? = null,
|
||||||
val compressionMode: CompressionMode? = null,
|
val compressionMode: CompressionMode? = null,
|
||||||
val compressionLevel: Int,
|
val compressionLevel: Int,
|
||||||
val chunkSize : Int
|
|
||||||
) : Configuration.Cache {
|
) : Configuration.Cache {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = createLogger<MemcacheCacheConfiguration>()
|
||||||
|
}
|
||||||
|
|
||||||
enum class CompressionMode {
|
enum class CompressionMode {
|
||||||
/**
|
/**
|
||||||
* Deflate mode
|
* Deflate mode
|
||||||
@@ -23,19 +38,64 @@ data class MemcacheCacheConfiguration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Server(
|
data class Server(
|
||||||
val endpoint : HostAndPort,
|
val endpoint: HostAndPort,
|
||||||
val connectionTimeoutMillis : Int?,
|
val connectionTimeoutMillis: Int?,
|
||||||
val maxConnections : Int
|
val maxConnections: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
override fun materialize() = object : CacheHandlerFactory {
|
override fun materialize() = object : CacheHandlerFactory {
|
||||||
private val client = MemcacheClient(this@MemcacheCacheConfiguration.servers, chunkSize)
|
|
||||||
override fun close() {
|
private val connectionPoolMap = ConcurrentHashMap<HostAndPort, FixedChannelPool>()
|
||||||
client.close()
|
|
||||||
|
override fun newHandler(
|
||||||
|
cfg : Configuration,
|
||||||
|
eventLoop: EventLoopGroup,
|
||||||
|
socketChannelFactory: ChannelFactory<SocketChannel>,
|
||||||
|
datagramChannelFactory: ChannelFactory<DatagramChannel>,
|
||||||
|
): CacheHandler {
|
||||||
|
return MemcacheCacheHandler(
|
||||||
|
MemcacheClient(
|
||||||
|
this@MemcacheCacheConfiguration.servers,
|
||||||
|
cfg.connection.chunkSize,
|
||||||
|
eventLoop,
|
||||||
|
socketChannelFactory,
|
||||||
|
connectionPoolMap
|
||||||
|
),
|
||||||
|
keyPrefix,
|
||||||
|
digestAlgorithm,
|
||||||
|
compressionMode != null,
|
||||||
|
compressionLevel,
|
||||||
|
cfg.connection.chunkSize,
|
||||||
|
maxAge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun asyncClose() = object : CompletableFuture<Void>() {
|
||||||
|
init {
|
||||||
|
val failure = AtomicReference<Throwable>(null)
|
||||||
|
val pools = connectionPoolMap.values.toList()
|
||||||
|
val npools = pools.size
|
||||||
|
val finished = AtomicInteger(0)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newHandler() = MemcacheCacheHandler(client, digestAlgorithm, compressionMode != null, compressionLevel, chunkSize, maxAge)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNamespaceURI() = "urn:net.woggioni.rbcs.server.memcache"
|
override fun getNamespaceURI() = "urn:net.woggioni.rbcs.server.memcache"
|
||||||
|
@@ -3,8 +3,8 @@ package net.woggioni.rbcs.server.memcache
|
|||||||
import io.netty.buffer.ByteBuf
|
import io.netty.buffer.ByteBuf
|
||||||
import io.netty.buffer.ByteBufAllocator
|
import io.netty.buffer.ByteBufAllocator
|
||||||
import io.netty.buffer.CompositeByteBuf
|
import io.netty.buffer.CompositeByteBuf
|
||||||
|
import io.netty.channel.Channel as NettyChannel
|
||||||
import io.netty.channel.ChannelHandlerContext
|
import io.netty.channel.ChannelHandlerContext
|
||||||
import io.netty.channel.SimpleChannelInboundHandler
|
|
||||||
import io.netty.handler.codec.memcache.DefaultLastMemcacheContent
|
import io.netty.handler.codec.memcache.DefaultLastMemcacheContent
|
||||||
import io.netty.handler.codec.memcache.DefaultMemcacheContent
|
import io.netty.handler.codec.memcache.DefaultMemcacheContent
|
||||||
import io.netty.handler.codec.memcache.LastMemcacheContent
|
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.BinaryMemcacheResponse
|
||||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponseStatus
|
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponseStatus
|
||||||
import io.netty.handler.codec.memcache.binary.DefaultBinaryMemcacheRequest
|
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.CacheValueMetadata
|
||||||
import net.woggioni.rbcs.api.exception.ContentTooLargeException
|
import net.woggioni.rbcs.api.exception.ContentTooLargeException
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage
|
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.MemcacheClient
|
||||||
import net.woggioni.rbcs.server.memcache.client.MemcacheRequestController
|
import net.woggioni.rbcs.server.memcache.client.MemcacheRequestController
|
||||||
import net.woggioni.rbcs.server.memcache.client.MemcacheResponseHandler
|
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(
|
class MemcacheCacheHandler(
|
||||||
private val client: MemcacheClient,
|
private val client: MemcacheClient,
|
||||||
|
private val keyPrefix: String?,
|
||||||
private val digestAlgorithm: String?,
|
private val digestAlgorithm: String?,
|
||||||
private val compressionEnabled: Boolean,
|
private val compressionEnabled: Boolean,
|
||||||
private val compressionLevel: Int,
|
private val compressionLevel: Int,
|
||||||
private val chunkSize: Int,
|
private val chunkSize: Int,
|
||||||
private val maxAge: Duration
|
private val maxAge: Duration
|
||||||
) : SimpleChannelInboundHandler<CacheMessage>() {
|
) : CacheHandler() {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = createLogger<MemcacheCacheHandler>()
|
private val log = createLogger<MemcacheCacheHandler>()
|
||||||
|
|
||||||
@@ -69,10 +70,14 @@ class MemcacheCacheHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private interface InProgressRequest {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private inner class InProgressGetRequest(
|
private inner class InProgressGetRequest(
|
||||||
private val key: String,
|
val key: String,
|
||||||
private val ctx: ChannelHandlerContext
|
private val ctx: ChannelHandlerContext
|
||||||
) {
|
) : InProgressRequest {
|
||||||
private val acc = ctx.alloc().compositeBuffer()
|
private val acc = ctx.alloc().compositeBuffer()
|
||||||
private val chunk = ctx.alloc().compositeBuffer()
|
private val chunk = ctx.alloc().compositeBuffer()
|
||||||
private val outputStream = ByteBufOutputStream(chunk).let {
|
private val outputStream = ByteBufOutputStream(chunk).let {
|
||||||
@@ -98,32 +103,35 @@ class MemcacheCacheHandler(
|
|||||||
acc.retain()
|
acc.retain()
|
||||||
it.readObject() as CacheValueMetadata
|
it.readObject() as CacheValueMetadata
|
||||||
}
|
}
|
||||||
ctx.writeAndFlush(CacheValueFoundResponse(key, metadata))
|
log.trace(ctx) {
|
||||||
|
"Sending response from cache"
|
||||||
|
}
|
||||||
|
sendMessageAndFlush(ctx, CacheValueFoundResponse(key, metadata))
|
||||||
responseSent = true
|
responseSent = true
|
||||||
acc.readerIndex(Int.SIZE_BYTES + mSize)
|
acc.readerIndex(Int.SIZE_BYTES + mSize)
|
||||||
}
|
}
|
||||||
if (responseSent) {
|
if (responseSent) {
|
||||||
acc.readBytes(outputStream, acc.readableBytes())
|
acc.readBytes(outputStream, acc.readableBytes())
|
||||||
if(acc.readableBytes() >= chunkSize) {
|
if (acc.readableBytes() >= chunkSize) {
|
||||||
flush(false)
|
flush(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flush(last : Boolean) {
|
private fun flush(last: Boolean) {
|
||||||
val toSend = extractChunk(chunk, ctx.alloc())
|
val toSend = extractChunk(chunk, ctx.alloc())
|
||||||
val msg = if(last) {
|
val msg = if (last) {
|
||||||
log.trace(ctx) {
|
log.trace(ctx) {
|
||||||
"Sending last chunk to client on channel ${ctx.channel().id().asShortText()}"
|
"Sending last chunk to client"
|
||||||
}
|
}
|
||||||
LastCacheContent(toSend)
|
LastCacheContent(toSend)
|
||||||
} else {
|
} else {
|
||||||
log.trace(ctx) {
|
log.trace(ctx) {
|
||||||
"Sending chunk to client on channel ${ctx.channel().id().asShortText()}"
|
"Sending chunk to client"
|
||||||
}
|
}
|
||||||
CacheContent(toSend)
|
CacheContent(toSend)
|
||||||
}
|
}
|
||||||
ctx.writeAndFlush(msg)
|
sendMessageAndFlush(ctx, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun commit() {
|
fun commit() {
|
||||||
@@ -141,14 +149,14 @@ class MemcacheCacheHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private inner class InProgressPutRequest(
|
private inner class InProgressPutRequest(
|
||||||
private val ch : NettyChannel,
|
private val ch: NettyChannel,
|
||||||
metadata : CacheValueMetadata,
|
metadata: CacheValueMetadata,
|
||||||
val digest : ByteBuf,
|
val digest: ByteBuf,
|
||||||
val requestController: CompletableFuture<MemcacheRequestController>,
|
val requestController: CompletableFuture<MemcacheRequestController>,
|
||||||
private val alloc: ByteBufAllocator
|
private val alloc: ByteBufAllocator
|
||||||
) {
|
) : InProgressRequest {
|
||||||
private var totalSize = 0
|
private var totalSize = 0
|
||||||
private var tmpFile : FileChannel? = null
|
private var tmpFile: FileChannel? = null
|
||||||
private val accumulator = alloc.compositeBuffer()
|
private val accumulator = alloc.compositeBuffer()
|
||||||
private val stream = ByteBufOutputStream(accumulator).let {
|
private val stream = ByteBufOutputStream(accumulator).let {
|
||||||
if (compressionEnabled) {
|
if (compressionEnabled) {
|
||||||
@@ -175,7 +183,7 @@ class MemcacheCacheHandler(
|
|||||||
tmpFile?.let {
|
tmpFile?.let {
|
||||||
flushToDisk(it, accumulator)
|
flushToDisk(it, accumulator)
|
||||||
}
|
}
|
||||||
if(accumulator.readableBytes() > 0x100000) {
|
if (accumulator.readableBytes() > 0x100000) {
|
||||||
log.debug(ch) {
|
log.debug(ch) {
|
||||||
"Entry is too big, buffering it into a file"
|
"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)
|
val chunk = extractChunk(buf, alloc)
|
||||||
fc.write(chunk.nioBuffer())
|
fc.write(chunk.nioBuffer())
|
||||||
chunk.release()
|
chunk.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun commit() : Pair<Int, ReadableByteChannel> {
|
fun commit(): Pair<Int, ReadableByteChannel> {
|
||||||
digest.release()
|
digest.release()
|
||||||
accumulator.retain()
|
accumulator.retain()
|
||||||
stream.close()
|
stream.close()
|
||||||
val fileChannel = tmpFile
|
val fileChannel = tmpFile
|
||||||
return if(fileChannel != null) {
|
return if (fileChannel != null) {
|
||||||
flushToDisk(fileChannel, accumulator)
|
flushToDisk(fileChannel, accumulator)
|
||||||
accumulator.release()
|
accumulator.release()
|
||||||
fileChannel.position(0)
|
fileChannel.position(0)
|
||||||
@@ -224,8 +232,7 @@ class MemcacheCacheHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var inProgressPutRequest: InProgressPutRequest? = null
|
private var inProgressRequest: InProgressRequest? = null
|
||||||
private var inProgressGetRequest: InProgressGetRequest? = null
|
|
||||||
|
|
||||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
|
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
|
||||||
when (msg) {
|
when (msg) {
|
||||||
@@ -242,7 +249,7 @@ class MemcacheCacheHandler(
|
|||||||
"Fetching ${msg.key} from memcache"
|
"Fetching ${msg.key} from memcache"
|
||||||
}
|
}
|
||||||
val key = ctx.alloc().buffer().also {
|
val key = ctx.alloc().buffer().also {
|
||||||
it.writeBytes(processCacheKey(msg.key, digestAlgorithm))
|
it.writeBytes(processCacheKey(msg.key, keyPrefix, digestAlgorithm))
|
||||||
}
|
}
|
||||||
val responseHandler = object : MemcacheResponseHandler {
|
val responseHandler = object : MemcacheResponseHandler {
|
||||||
override fun responseReceived(response: BinaryMemcacheResponse) {
|
override fun responseReceived(response: BinaryMemcacheResponse) {
|
||||||
@@ -252,32 +259,39 @@ class MemcacheCacheHandler(
|
|||||||
log.debug(ctx) {
|
log.debug(ctx) {
|
||||||
"Cache hit for key ${msg.key} on memcache"
|
"Cache hit for key ${msg.key} on memcache"
|
||||||
}
|
}
|
||||||
inProgressGetRequest = InProgressGetRequest(msg.key, ctx)
|
inProgressRequest = InProgressGetRequest(msg.key, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
BinaryMemcacheResponseStatus.KEY_ENOENT -> {
|
BinaryMemcacheResponseStatus.KEY_ENOENT -> {
|
||||||
log.debug(ctx) {
|
log.debug(ctx) {
|
||||||
"Cache miss for key ${msg.key} on memcache"
|
"Cache miss for key ${msg.key} on memcache"
|
||||||
}
|
}
|
||||||
ctx.writeAndFlush(CacheValueNotFoundResponse())
|
sendMessageAndFlush(ctx, CacheValueNotFoundResponse(msg.key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentReceived(content: MemcacheContent) {
|
override fun contentReceived(content: MemcacheContent) {
|
||||||
log.trace(ctx) {
|
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())
|
(inProgressRequest as? InProgressGetRequest)?.let { inProgressGetRequest ->
|
||||||
if (content is LastMemcacheContent) {
|
inProgressGetRequest.write(content.content())
|
||||||
inProgressGetRequest?.commit()
|
if (content is LastMemcacheContent) {
|
||||||
|
inProgressRequest = null
|
||||||
|
inProgressGetRequest.commit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exceptionCaught(ex: Throwable) {
|
override fun exceptionCaught(ex: Throwable) {
|
||||||
inProgressGetRequest?.let {
|
(inProgressRequest as? InProgressGetRequest).let { inProgressGetRequest ->
|
||||||
inProgressGetRequest = null
|
inProgressGetRequest?.let {
|
||||||
it.rollback()
|
inProgressRequest = null
|
||||||
|
it.rollback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this@MemcacheCacheHandler.exceptionCaught(ctx, ex)
|
this@MemcacheCacheHandler.exceptionCaught(ctx, ex)
|
||||||
}
|
}
|
||||||
@@ -290,12 +304,13 @@ class MemcacheCacheHandler(
|
|||||||
setOpcode(BinaryMemcacheOpcodes.GET)
|
setOpcode(BinaryMemcacheOpcodes.GET)
|
||||||
}
|
}
|
||||||
requestHandle.sendRequest(request)
|
requestHandle.sendRequest(request)
|
||||||
|
requestHandle.sendContent(LastMemcacheContent.EMPTY_LAST_CONTENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
|
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
|
||||||
val key = ctx.alloc().buffer().also {
|
val key = ctx.alloc().buffer().also {
|
||||||
it.writeBytes(processCacheKey(msg.key, digestAlgorithm))
|
it.writeBytes(processCacheKey(msg.key, keyPrefix, digestAlgorithm))
|
||||||
}
|
}
|
||||||
val responseHandler = object : MemcacheResponseHandler {
|
val responseHandler = object : MemcacheResponseHandler {
|
||||||
override fun responseReceived(response: BinaryMemcacheResponse) {
|
override fun responseReceived(response: BinaryMemcacheResponse) {
|
||||||
@@ -305,8 +320,9 @@ class MemcacheCacheHandler(
|
|||||||
log.debug(ctx) {
|
log.debug(ctx) {
|
||||||
"Inserted key ${msg.key} into memcache"
|
"Inserted key ${msg.key} into memcache"
|
||||||
}
|
}
|
||||||
ctx.writeAndFlush(CachePutResponse(msg.key))
|
sendMessageAndFlush(ctx, CachePutResponse(msg.key))
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> this@MemcacheCacheHandler.exceptionCaught(ctx, MemcacheException(status))
|
else -> this@MemcacheCacheHandler.exceptionCaught(ctx, MemcacheException(status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,86 +339,103 @@ class MemcacheCacheHandler(
|
|||||||
this@MemcacheCacheHandler.exceptionCaught(ctx, ex)
|
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) {
|
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
|
||||||
inProgressPutRequest?.let { request ->
|
val request = inProgressRequest
|
||||||
log.trace(ctx) {
|
when (request) {
|
||||||
"Received chunk of ${msg.content().readableBytes()} bytes for memcache"
|
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) {
|
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
|
||||||
inProgressPutRequest?.let { request ->
|
val request = inProgressRequest
|
||||||
inProgressPutRequest = null
|
when (request) {
|
||||||
log.trace(ctx) {
|
is InProgressPutRequest -> {
|
||||||
"Received last chunk of ${msg.content().readableBytes()} bytes for memcache"
|
inProgressRequest = null
|
||||||
}
|
log.trace(ctx) {
|
||||||
request.write(msg.content())
|
"Received last chunk of ${msg.content().readableBytes()} bytes for memcache"
|
||||||
val key = request.digest.retainedDuplicate()
|
}
|
||||||
val (payloadSize, payloadSource) = request.commit()
|
request.write(msg.content())
|
||||||
val extras = ctx.alloc().buffer(8, 8)
|
val key = request.digest.retainedDuplicate()
|
||||||
extras.writeInt(0)
|
val (payloadSize, payloadSource) = request.commit()
|
||||||
extras.writeInt(encodeExpiry(maxAge))
|
val extras = ctx.alloc().buffer(8, 8)
|
||||||
val totalBodyLength = request.digest.readableBytes() + extras.readableBytes() + payloadSize
|
extras.writeInt(0)
|
||||||
request.requestController.whenComplete { requestController, ex ->
|
extras.writeInt(encodeExpiry(maxAge))
|
||||||
if(ex == null) {
|
val totalBodyLength = request.digest.readableBytes() + extras.readableBytes() + payloadSize
|
||||||
log.trace(ctx) {
|
log.trace(ctx) {
|
||||||
"Sending SET request to memcache"
|
"Trying to send SET request to memcache"
|
||||||
}
|
}
|
||||||
requestController.sendRequest(DefaultBinaryMemcacheRequest().apply {
|
request.requestController.whenComplete { requestController, ex ->
|
||||||
setOpcode(BinaryMemcacheOpcodes.SET)
|
if (ex == null) {
|
||||||
setKey(key)
|
log.trace(ctx) {
|
||||||
setExtras(extras)
|
"Sending SET request to memcache"
|
||||||
setTotalBodyLength(totalBodyLength)
|
}
|
||||||
})
|
requestController.sendRequest(DefaultBinaryMemcacheRequest().apply {
|
||||||
log.trace(ctx) {
|
setOpcode(BinaryMemcacheOpcodes.SET)
|
||||||
"Sending request payload to memcache"
|
setKey(key)
|
||||||
}
|
setExtras(extras)
|
||||||
payloadSource.use { source ->
|
setTotalBodyLength(totalBodyLength)
|
||||||
val bb = ByteBuffer.allocate(chunkSize)
|
})
|
||||||
while (true) {
|
log.trace(ctx) {
|
||||||
val read = source.read(bb)
|
"Sending request payload to memcache"
|
||||||
bb.limit()
|
}
|
||||||
if(read >= 0 && bb.position() < chunkSize && bb.hasRemaining()) {
|
payloadSource.use { source ->
|
||||||
continue
|
val bb = ByteBuffer.allocate(chunkSize)
|
||||||
}
|
while (true) {
|
||||||
val chunk = ctx.alloc().buffer(chunkSize)
|
val read = source.read(bb)
|
||||||
bb.flip()
|
bb.limit()
|
||||||
chunk.writeBytes(bb)
|
if (read >= 0 && bb.position() < chunkSize && bb.hasRemaining()) {
|
||||||
bb.clear()
|
continue
|
||||||
log.trace(ctx) {
|
}
|
||||||
"Sending ${chunk.readableBytes()} bytes chunk to memcache"
|
val chunk = ctx.alloc().buffer(chunkSize)
|
||||||
}
|
bb.flip()
|
||||||
if(read < 0) {
|
chunk.writeBytes(bb)
|
||||||
requestController.sendContent(DefaultLastMemcacheContent(chunk))
|
bb.clear()
|
||||||
break
|
log.trace(ctx) {
|
||||||
} else {
|
"Sending ${chunk.readableBytes()} bytes chunk to memcache"
|
||||||
requestController.sendContent(DefaultMemcacheContent(chunk))
|
}
|
||||||
|
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) {
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||||
inProgressGetRequest?.let {
|
val request = inProgressRequest
|
||||||
inProgressGetRequest = null
|
when (request) {
|
||||||
it.rollback()
|
is InProgressPutRequest -> {
|
||||||
}
|
inProgressRequest = null
|
||||||
inProgressPutRequest?.let {
|
request.requestController.thenAccept { controller ->
|
||||||
inProgressPutRequest = null
|
controller.exceptionCaught(cause)
|
||||||
it.requestController.thenAccept { controller ->
|
}
|
||||||
controller.exceptionCaught(cause)
|
request.rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
is InProgressGetRequest -> {
|
||||||
|
inProgressRequest = null
|
||||||
|
request.rollback()
|
||||||
}
|
}
|
||||||
it.rollback()
|
|
||||||
}
|
}
|
||||||
super.exceptionCaught(ctx, cause)
|
super.exceptionCaught(ctx, cause)
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package net.woggioni.rbcs.server.memcache
|
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.CacheProvider
|
||||||
import net.woggioni.rbcs.api.exception.ConfigurationException
|
import net.woggioni.rbcs.api.exception.ConfigurationException
|
||||||
import net.woggioni.rbcs.common.HostAndPort
|
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 net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||||
import org.w3c.dom.Document
|
import org.w3c.dom.Document
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import java.time.Duration
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
|
|
||||||
|
|
||||||
class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
||||||
@@ -28,9 +28,6 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
|||||||
val maxAge = el.renderAttribute("max-age")
|
val maxAge = el.renderAttribute("max-age")
|
||||||
?.let(Duration::parse)
|
?.let(Duration::parse)
|
||||||
?: Duration.ofDays(1)
|
?: Duration.ofDays(1)
|
||||||
val chunkSize = el.renderAttribute("chunk-size")
|
|
||||||
?.let(Integer::decode)
|
|
||||||
?: 0x10000
|
|
||||||
val compressionLevel = el.renderAttribute("compression-level")
|
val compressionLevel = el.renderAttribute("compression-level")
|
||||||
?.let(Integer::decode)
|
?.let(Integer::decode)
|
||||||
?: -1
|
?: -1
|
||||||
@@ -41,6 +38,7 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
|||||||
else -> MemcacheCacheConfiguration.CompressionMode.DEFLATE
|
else -> MemcacheCacheConfiguration.CompressionMode.DEFLATE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val keyPrefix = el.renderAttribute("key-prefix")
|
||||||
val digestAlgorithm = el.renderAttribute("digest")
|
val digestAlgorithm = el.renderAttribute("digest")
|
||||||
for (child in el.asIterable()) {
|
for (child in el.asIterable()) {
|
||||||
when (child.nodeName) {
|
when (child.nodeName) {
|
||||||
@@ -57,14 +55,13 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MemcacheCacheConfiguration(
|
return MemcacheCacheConfiguration(
|
||||||
servers,
|
servers,
|
||||||
maxAge,
|
maxAge,
|
||||||
|
keyPrefix,
|
||||||
digestAlgorithm,
|
digestAlgorithm,
|
||||||
compressionMode,
|
compressionMode,
|
||||||
compressionLevel,
|
compressionLevel
|
||||||
chunkSize
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,9 +79,12 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
|
|||||||
}
|
}
|
||||||
attr("max-connections", server.maxConnections.toString())
|
attr("max-connections", server.maxConnections.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
attr("max-age", maxAge.toString())
|
attr("max-age", maxAge.toString())
|
||||||
attr("chunk-size", chunkSize.toString())
|
keyPrefix?.let {
|
||||||
|
attr("key-prefix", it)
|
||||||
|
}
|
||||||
digestAlgorithm?.let { digestAlgorithm ->
|
digestAlgorithm?.let { digestAlgorithm ->
|
||||||
attr("digest", digestAlgorithm)
|
attr("digest", digestAlgorithm)
|
||||||
}
|
}
|
||||||
|
@@ -4,52 +4,51 @@ package net.woggioni.rbcs.server.memcache.client
|
|||||||
import io.netty.bootstrap.Bootstrap
|
import io.netty.bootstrap.Bootstrap
|
||||||
import io.netty.buffer.ByteBuf
|
import io.netty.buffer.ByteBuf
|
||||||
import io.netty.channel.Channel
|
import io.netty.channel.Channel
|
||||||
|
import io.netty.channel.ChannelFactory
|
||||||
import io.netty.channel.ChannelFutureListener
|
import io.netty.channel.ChannelFutureListener
|
||||||
import io.netty.channel.ChannelHandlerContext
|
import io.netty.channel.ChannelHandlerContext
|
||||||
import io.netty.channel.ChannelOption
|
import io.netty.channel.ChannelOption
|
||||||
import io.netty.channel.ChannelPipeline
|
import io.netty.channel.ChannelPipeline
|
||||||
|
import io.netty.channel.EventLoopGroup
|
||||||
import io.netty.channel.SimpleChannelInboundHandler
|
import io.netty.channel.SimpleChannelInboundHandler
|
||||||
import io.netty.channel.nio.NioEventLoopGroup
|
|
||||||
import io.netty.channel.pool.AbstractChannelPoolHandler
|
import io.netty.channel.pool.AbstractChannelPoolHandler
|
||||||
import io.netty.channel.pool.ChannelPool
|
|
||||||
import io.netty.channel.pool.FixedChannelPool
|
import io.netty.channel.pool.FixedChannelPool
|
||||||
import io.netty.channel.socket.nio.NioSocketChannel
|
import io.netty.channel.socket.SocketChannel
|
||||||
import io.netty.handler.codec.memcache.LastMemcacheContent
|
import io.netty.handler.codec.memcache.LastMemcacheContent
|
||||||
import io.netty.handler.codec.memcache.MemcacheContent
|
import io.netty.handler.codec.memcache.MemcacheContent
|
||||||
import io.netty.handler.codec.memcache.MemcacheObject
|
import io.netty.handler.codec.memcache.MemcacheObject
|
||||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec
|
import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec
|
||||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheRequest
|
import io.netty.handler.codec.memcache.binary.BinaryMemcacheRequest
|
||||||
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse
|
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse
|
||||||
|
import io.netty.util.concurrent.Future as NettyFuture
|
||||||
import io.netty.util.concurrent.GenericFutureListener
|
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.io.IOException
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
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(private val servers: List<MemcacheCacheConfiguration.Server>, private val chunkSize : Int) : AutoCloseable {
|
class MemcacheClient(
|
||||||
|
private val servers: List<MemcacheCacheConfiguration.Server>,
|
||||||
|
private val chunkSize : Int,
|
||||||
|
private val group: EventLoopGroup,
|
||||||
|
private val channelFactory: ChannelFactory<SocketChannel>,
|
||||||
|
private val connectionPool: ConcurrentHashMap<HostAndPort, FixedChannelPool>
|
||||||
|
) : AutoCloseable {
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private val log = createLogger<MemcacheCacheHandler>()
|
private val log = createLogger<MemcacheCacheHandler>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val group: NioEventLoopGroup
|
|
||||||
private val connectionPool: MutableMap<HostAndPort, ChannelPool> = ConcurrentHashMap()
|
|
||||||
|
|
||||||
init {
|
|
||||||
group = NioEventLoopGroup()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newConnectionPool(server: MemcacheCacheConfiguration.Server): FixedChannelPool {
|
private fun newConnectionPool(server: MemcacheCacheConfiguration.Server): FixedChannelPool {
|
||||||
val bootstrap = Bootstrap().apply {
|
val bootstrap = Bootstrap().apply {
|
||||||
group(group)
|
group(group)
|
||||||
channel(NioSocketChannel::class.java)
|
channelFactory(channelFactory)
|
||||||
option(ChannelOption.SO_KEEPALIVE, true)
|
option(ChannelOption.SO_KEEPALIVE, true)
|
||||||
remoteAddress(InetSocketAddress(server.endpoint.host, server.endpoint.port))
|
remoteAddress(InetSocketAddress(server.endpoint.host, server.endpoint.port))
|
||||||
server.connectionTimeoutMillis?.let {
|
server.connectionTimeoutMillis?.let {
|
||||||
@@ -94,18 +93,6 @@ class MemcacheClient(private val servers: List<MemcacheCacheConfiguration.Server
|
|||||||
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
||||||
override fun operationComplete(channelFuture: NettyFuture<Channel>) {
|
override fun operationComplete(channelFuture: NettyFuture<Channel>) {
|
||||||
if (channelFuture.isSuccess) {
|
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
|
val channel = channelFuture.now
|
||||||
var connectionClosedByTheRemoteServer = true
|
var connectionClosedByTheRemoteServer = true
|
||||||
val closeCallback = {
|
val closeCallback = {
|
||||||
@@ -113,14 +100,7 @@ class MemcacheClient(private val servers: List<MemcacheCacheConfiguration.Server
|
|||||||
val ex = IOException("The memcache server closed the connection")
|
val ex = IOException("The memcache server closed the connection")
|
||||||
val completed = response.completeExceptionally(ex)
|
val completed = response.completeExceptionally(ex)
|
||||||
if(!completed) responseHandler.exceptionCaught(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 {
|
val closeListener = ChannelFutureListener {
|
||||||
closeCallback()
|
closeCallback()
|
||||||
@@ -140,18 +120,14 @@ class MemcacheClient(private val servers: List<MemcacheCacheConfiguration.Server
|
|||||||
when (msg) {
|
when (msg) {
|
||||||
is BinaryMemcacheResponse -> {
|
is BinaryMemcacheResponse -> {
|
||||||
responseHandler.responseReceived(msg)
|
responseHandler.responseReceived(msg)
|
||||||
responseReceived = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is LastMemcacheContent -> {
|
is LastMemcacheContent -> {
|
||||||
responseFinished = true
|
|
||||||
responseHandler.contentReceived(msg)
|
responseHandler.contentReceived(msg)
|
||||||
pipeline.remove(this)
|
pipeline.remove(this)
|
||||||
pool.release(channel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is MemcacheContent -> {
|
is MemcacheContent -> {
|
||||||
responseBodyReceived = true
|
|
||||||
responseHandler.contentReceived(msg)
|
responseHandler.contentReceived(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,35 +141,43 @@ class MemcacheClient(private val servers: List<MemcacheCacheConfiguration.Server
|
|||||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||||
connectionClosedByTheRemoteServer = false
|
connectionClosedByTheRemoteServer = false
|
||||||
ctx.close()
|
ctx.close()
|
||||||
pool.release(channel)
|
|
||||||
responseHandler.exceptionCaught(cause)
|
responseHandler.exceptionCaught(cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.pipeline()
|
channel.pipeline().addLast(handler)
|
||||||
.addLast("client-handler", handler)
|
|
||||||
response.complete(object : MemcacheRequestController {
|
response.complete(object : MemcacheRequestController {
|
||||||
|
private var channelReleased = false
|
||||||
|
|
||||||
override fun sendRequest(request: BinaryMemcacheRequest) {
|
override fun sendRequest(request: BinaryMemcacheRequest) {
|
||||||
requestBodySize = request.totalBodyLength() - request.keyLength() - request.extrasLength()
|
|
||||||
channel.writeAndFlush(request)
|
channel.writeAndFlush(request)
|
||||||
requestSent = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendContent(content: MemcacheContent) {
|
override fun sendContent(content: MemcacheContent) {
|
||||||
val size = content.content().readableBytes()
|
|
||||||
channel.writeAndFlush(content).addListener {
|
channel.writeAndFlush(content).addListener {
|
||||||
requestBodyBytesSent += size
|
|
||||||
requestBodySent = true
|
|
||||||
if(content is LastMemcacheContent) {
|
if(content is LastMemcacheContent) {
|
||||||
requestFinished = true
|
if(!channelReleased) {
|
||||||
|
pool.release(channel)
|
||||||
|
channelReleased = true
|
||||||
|
log.trace(channel) {
|
||||||
|
"Channel released"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exceptionCaught(ex: Throwable) {
|
override fun exceptionCaught(ex: Throwable) {
|
||||||
|
log.warn(ex.message, ex)
|
||||||
connectionClosedByTheRemoteServer = false
|
connectionClosedByTheRemoteServer = false
|
||||||
channel.close()
|
channel.close()
|
||||||
|
if(!channelReleased) {
|
||||||
|
pool.release(channel)
|
||||||
|
channelReleased = true
|
||||||
|
log.trace(channel) {
|
||||||
|
"Channel released"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
xmlns:rbcs="urn:net.woggioni.rbcs.server"
|
xmlns:rbcs="urn:net.woggioni.rbcs.server"
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||||
|
|
||||||
<xs:import schemaLocation="jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs.xsd" namespace="urn:net.woggioni.rbcs.server"/>
|
<xs:import schemaLocation="jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd" namespace="urn:net.woggioni.rbcs.server"/>
|
||||||
|
|
||||||
<xs:complexType name="memcacheServerType">
|
<xs:complexType name="memcacheServerType">
|
||||||
<xs:attribute name="host" type="xs:token" use="required"/>
|
<xs:attribute name="host" type="xs:token" use="required"/>
|
||||||
@@ -21,7 +21,15 @@
|
|||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
|
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
|
||||||
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
|
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
|
||||||
<xs:attribute name="digest" type="xs:token" />
|
<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-mode" type="rbcs-memcache:compressionType"/>
|
||||||
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/>
|
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/>
|
||||||
</xs:extension>
|
</xs:extension>
|
||||||
|
@@ -2,12 +2,12 @@ package net.woggioni.rbcs.server.memcache.client
|
|||||||
|
|
||||||
import io.netty.buffer.ByteBufUtil
|
import io.netty.buffer.ByteBufUtil
|
||||||
import io.netty.buffer.Unpooled
|
import io.netty.buffer.Unpooled
|
||||||
import org.junit.jupiter.api.Assertions
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.channels.Channels
|
import java.nio.channels.Channels
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class ByteBufferTest {
|
class ByteBufferTest {
|
||||||
|
|
||||||
|
@@ -3,27 +3,27 @@ import net.woggioni.rbcs.server.cache.FileSystemCacheProvider;
|
|||||||
import net.woggioni.rbcs.server.cache.InMemoryCacheProvider;
|
import net.woggioni.rbcs.server.cache.InMemoryCacheProvider;
|
||||||
|
|
||||||
module net.woggioni.rbcs.server {
|
module net.woggioni.rbcs.server {
|
||||||
requires java.sql;
|
|
||||||
requires java.xml;
|
requires java.xml;
|
||||||
requires java.logging;
|
|
||||||
requires java.naming;
|
requires java.naming;
|
||||||
requires kotlin.stdlib;
|
requires kotlin.stdlib;
|
||||||
requires io.netty.buffer;
|
|
||||||
requires io.netty.transport;
|
|
||||||
requires io.netty.codec.http;
|
requires io.netty.codec.http;
|
||||||
requires io.netty.common;
|
|
||||||
requires io.netty.handler;
|
requires io.netty.handler;
|
||||||
requires io.netty.codec;
|
|
||||||
requires org.slf4j;
|
|
||||||
requires net.woggioni.jwo;
|
requires net.woggioni.jwo;
|
||||||
requires net.woggioni.rbcs.common;
|
requires net.woggioni.rbcs.common;
|
||||||
requires net.woggioni.rbcs.api;
|
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;
|
exports net.woggioni.rbcs.server;
|
||||||
|
|
||||||
opens net.woggioni.rbcs.server;
|
opens net.woggioni.rbcs.server;
|
||||||
opens net.woggioni.rbcs.server.schema;
|
opens net.woggioni.rbcs.server.schema;
|
||||||
|
|
||||||
|
|
||||||
uses CacheProvider;
|
uses CacheProvider;
|
||||||
provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider;
|
provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider;
|
||||||
}
|
}
|
@@ -3,6 +3,7 @@ package net.woggioni.rbcs.server
|
|||||||
import io.netty.bootstrap.ServerBootstrap
|
import io.netty.bootstrap.ServerBootstrap
|
||||||
import io.netty.buffer.ByteBuf
|
import io.netty.buffer.ByteBuf
|
||||||
import io.netty.channel.Channel
|
import io.netty.channel.Channel
|
||||||
|
import io.netty.channel.ChannelFactory
|
||||||
import io.netty.channel.ChannelFuture
|
import io.netty.channel.ChannelFuture
|
||||||
import io.netty.channel.ChannelHandler.Sharable
|
import io.netty.channel.ChannelHandler.Sharable
|
||||||
import io.netty.channel.ChannelHandlerContext
|
import io.netty.channel.ChannelHandlerContext
|
||||||
@@ -10,11 +11,18 @@ import io.netty.channel.ChannelInboundHandlerAdapter
|
|||||||
import io.netty.channel.ChannelInitializer
|
import io.netty.channel.ChannelInitializer
|
||||||
import io.netty.channel.ChannelOption
|
import io.netty.channel.ChannelOption
|
||||||
import io.netty.channel.ChannelPromise
|
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
|
||||||
|
import io.netty.channel.socket.nio.NioDatagramChannel
|
||||||
import io.netty.channel.socket.nio.NioServerSocketChannel
|
import io.netty.channel.socket.nio.NioServerSocketChannel
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel
|
||||||
import io.netty.handler.codec.compression.CompressionOptions
|
import io.netty.handler.codec.compression.CompressionOptions
|
||||||
import io.netty.handler.codec.http.DefaultHttpContent
|
import io.netty.handler.codec.http.DefaultHttpContent
|
||||||
import io.netty.handler.codec.http.HttpContentCompressor
|
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.HttpHeaderNames
|
||||||
import io.netty.handler.codec.http.HttpRequest
|
import io.netty.handler.codec.http.HttpRequest
|
||||||
import io.netty.handler.codec.http.HttpServerCodec
|
import io.netty.handler.codec.http.HttpServerCodec
|
||||||
@@ -27,36 +35,11 @@ import io.netty.handler.timeout.IdleState
|
|||||||
import io.netty.handler.timeout.IdleStateEvent
|
import io.netty.handler.timeout.IdleStateEvent
|
||||||
import io.netty.handler.timeout.IdleStateHandler
|
import io.netty.handler.timeout.IdleStateHandler
|
||||||
import io.netty.util.AttributeKey
|
import io.netty.util.AttributeKey
|
||||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
|
||||||
import io.netty.util.concurrent.EventExecutorGroup
|
import io.netty.util.concurrent.EventExecutorGroup
|
||||||
import net.woggioni.jwo.JWO
|
|
||||||
import net.woggioni.jwo.Tuple2
|
|
||||||
import net.woggioni.rbcs.api.Configuration
|
|
||||||
import net.woggioni.rbcs.api.exception.ConfigurationException
|
|
||||||
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
|
|
||||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
|
||||||
import net.woggioni.rbcs.common.RBCS.toUrl
|
|
||||||
import net.woggioni.rbcs.common.Xml
|
|
||||||
import net.woggioni.rbcs.common.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.io.OutputStream
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -71,6 +54,29 @@ import java.util.regex.Matcher
|
|||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.naming.ldap.LdapName
|
import javax.naming.ldap.LdapName
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException
|
import javax.net.ssl.SSLPeerUnverifiedException
|
||||||
|
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) {
|
class RemoteBuildCacheServer(private val cfg: Configuration) {
|
||||||
|
|
||||||
@@ -80,7 +86,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
|||||||
val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user")
|
val userAttribute: AttributeKey<Configuration.User> = AttributeKey.valueOf("user")
|
||||||
val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group")
|
val groupAttribute: AttributeKey<Set<Configuration.Group>> = AttributeKey.valueOf("group")
|
||||||
|
|
||||||
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/rbcs/server/rbcs-default.xml".toUrl() }
|
val DEFAULT_CONFIGURATION_URL by lazy { "jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/rbcs-default.xml".toUrl() }
|
||||||
private const val SSL_HANDLER_NAME = "sslHandler"
|
private const val SSL_HANDLER_NAME = "sslHandler"
|
||||||
|
|
||||||
fun loadConfiguration(configurationFile: Path): Configuration {
|
fun loadConfiguration(configurationFile: Path): Configuration {
|
||||||
@@ -200,8 +206,9 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
|||||||
|
|
||||||
private class ServerInitializer(
|
private class ServerInitializer(
|
||||||
private val cfg: Configuration,
|
private val cfg: Configuration,
|
||||||
private val eventExecutorGroup: EventExecutorGroup
|
private val channelFactory : ChannelFactory<SocketChannel>,
|
||||||
) : ChannelInitializer<Channel>(), AutoCloseable {
|
private val datagramChannelFactory : ChannelFactory<DatagramChannel>,
|
||||||
|
) : ChannelInitializer<Channel>(), AsyncCloseable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun createSslCtx(tls: Configuration.Tls): SslContext {
|
private fun createSslCtx(tls: Configuration.Tls): SslContext {
|
||||||
@@ -221,7 +228,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
|||||||
val clientAuth = tls.trustStore?.let { trustStore ->
|
val clientAuth = tls.trustStore?.let { trustStore ->
|
||||||
val ts = loadKeystore(trustStore.file, trustStore.password)
|
val ts = loadKeystore(trustStore.file, trustStore.password)
|
||||||
trustManager(
|
trustManager(
|
||||||
ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus)
|
getTrustManager(ts, trustStore.isCheckCertificateStatus)
|
||||||
)
|
)
|
||||||
if (trustStore.isRequireClientCertificate) ClientAuth.REQUIRE
|
if (trustStore.isRequireClientCertificate) ClientAuth.REQUIRE
|
||||||
else ClientAuth.OPTIONAL
|
else ClientAuth.OPTIONAL
|
||||||
@@ -231,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>()
|
private val log = createLogger<ServerInitializer>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,21 +298,9 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
|||||||
"Closed connection ${ch.id().asShortText()} with ${ch.remoteAddress()}"
|
"Closed connection ${ch.id().asShortText()} with ${ch.remoteAddress()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ch.config().isAutoRead = false
|
||||||
val pipeline = ch.pipeline()
|
val pipeline = ch.pipeline()
|
||||||
cfg.connection.also { conn ->
|
cfg.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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val readIdleTimeout = conn.readIdleTimeout.toMillis()
|
val readIdleTimeout = conn.readIdleTimeout.toMillis()
|
||||||
val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
|
val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
|
||||||
val idleTimeout = conn.idleTimeout.toMillis()
|
val idleTimeout = conn.idleTimeout.toMillis()
|
||||||
@@ -367,79 +341,86 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
|||||||
sslContext?.newHandler(ch.alloc())?.also {
|
sslContext?.newHandler(ch.alloc())?.also {
|
||||||
pipeline.addLast(SSL_HANDLER_NAME, it)
|
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(MaxRequestSizeHandler.NAME, MaxRequestSizeHandler(cfg.connection.maxRequestSize))
|
||||||
pipeline.addLast(HttpChunkContentCompressor(1024))
|
pipeline.addLast(HttpChunkContentCompressor(1024))
|
||||||
pipeline.addLast(ChunkedWriteHandler())
|
pipeline.addLast(ChunkedWriteHandler())
|
||||||
authenticator?.let {
|
authenticator?.let {
|
||||||
pipeline.addLast(it)
|
pipeline.addLast(it)
|
||||||
}
|
}
|
||||||
pipeline.addLast(ThrottlingHandler(bucketManager, cfg.connection))
|
pipeline.addLast(ThrottlingHandler(bucketManager,cfg.rateLimiter, cfg.connection))
|
||||||
|
|
||||||
val serverHandler = let {
|
val serverHandler = let {
|
||||||
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
|
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(ServerHandler.NAME, serverHandler)
|
||||||
pipeline.addLast(cacheHandlerFactory.newHandler())
|
pipeline.addLast(ExceptionHandler.NAME, ExceptionHandler)
|
||||||
pipeline.addLast(TraceHandler)
|
|
||||||
pipeline.addLast(ExceptionHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun asyncClose() = cacheHandlerFactory.asyncClose()
|
||||||
cacheHandlerFactory.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerHandle(
|
class ServerHandle(
|
||||||
closeFuture: ChannelFuture,
|
closeFuture: ChannelFuture,
|
||||||
private val bossGroup: EventExecutorGroup,
|
private val bossGroup: EventExecutorGroup,
|
||||||
private val executorGroups: Iterable<EventExecutorGroup>,
|
private val executorGroups: Iterable<EventExecutorGroup>,
|
||||||
private val serverInitializer: AutoCloseable,
|
private val serverInitializer: AsyncCloseable,
|
||||||
) : Future<Void> by from(closeFuture, executorGroups, serverInitializer) {
|
) : Future<Void> by from(closeFuture, bossGroup, executorGroups, serverInitializer) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = createLogger<ServerHandle>()
|
private val log = createLogger<ServerHandle>()
|
||||||
|
|
||||||
private fun from(
|
private fun from(
|
||||||
closeFuture: ChannelFuture,
|
closeFuture: ChannelFuture,
|
||||||
|
bossGroup: EventExecutorGroup,
|
||||||
executorGroups: Iterable<EventExecutorGroup>,
|
executorGroups: Iterable<EventExecutorGroup>,
|
||||||
serverInitializer: AutoCloseable
|
serverInitializer: AsyncCloseable
|
||||||
): CompletableFuture<Void> {
|
): CompletableFuture<Void> {
|
||||||
val result = CompletableFuture<Void>()
|
val result = CompletableFuture<Void>()
|
||||||
closeFuture.addListener {
|
closeFuture.addListener {
|
||||||
val errors = mutableListOf<Throwable>()
|
val errors = mutableListOf<Throwable>()
|
||||||
val deadline = Instant.now().plusSeconds(20)
|
val deadline = Instant.now().plusSeconds(20)
|
||||||
|
|
||||||
|
serverInitializer.asyncClose().whenCompleteAsync { _, ex ->
|
||||||
for (executorGroup in executorGroups) {
|
if(ex != null) {
|
||||||
val future = executorGroup.terminationFuture()
|
log.error(ex.message, ex)
|
||||||
try {
|
|
||||||
val now = Instant.now()
|
|
||||||
if (now > deadline) {
|
|
||||||
future.get(0, TimeUnit.SECONDS)
|
|
||||||
} else {
|
|
||||||
future.get(Duration.between(now, deadline).toMillis(), TimeUnit.MILLISECONDS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (te: TimeoutException) {
|
|
||||||
errors.addLast(te)
|
|
||||||
log.warn("Timeout while waiting for shutdown of $executorGroup", te)
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.warn(ex.message, ex)
|
|
||||||
errors.addLast(ex)
|
errors.addLast(ex)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
try {
|
executorGroups.forEach(EventExecutorGroup::shutdownGracefully)
|
||||||
serverInitializer.close()
|
bossGroup.terminationFuture().sync()
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.error(ex.message, ex)
|
for (executorGroup in executorGroups) {
|
||||||
errors.addLast(ex)
|
val future = executorGroup.terminationFuture()
|
||||||
}
|
try {
|
||||||
if(errors.isEmpty()) {
|
val now = Instant.now()
|
||||||
result.complete(null)
|
if (now > deadline) {
|
||||||
} else {
|
future.get(0, TimeUnit.SECONDS)
|
||||||
result.completeExceptionally(errors.first())
|
} else {
|
||||||
|
future.get(Duration.between(now, deadline).toMillis(), TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (te: TimeoutException) {
|
||||||
|
errors.addLast(te)
|
||||||
|
log.warn("Timeout while waiting for shutdown of $executorGroup", te)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.warn(ex.message, ex)
|
||||||
|
errors.addLast(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(errors.isEmpty()) {
|
||||||
|
result.complete(null)
|
||||||
|
} else {
|
||||||
|
result.completeExceptionally(errors.first())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,30 +435,22 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
|||||||
|
|
||||||
fun sendShutdownSignal() {
|
fun sendShutdownSignal() {
|
||||||
bossGroup.shutdownGracefully()
|
bossGroup.shutdownGracefully()
|
||||||
executorGroups.map {
|
|
||||||
it.shutdownGracefully()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun run(): ServerHandle {
|
fun run(): ServerHandle {
|
||||||
// Create the multithreaded event loops for the server
|
// Create the multithreaded event loops for the server
|
||||||
val bossGroup = NioEventLoopGroup(1)
|
val bossGroup = MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory())
|
||||||
val serverSocketChannel = NioServerSocketChannel::class.java
|
val channelFactory = ChannelFactory<SocketChannel> { NioSocketChannel() }
|
||||||
val workerGroup = NioEventLoopGroup(0)
|
val datagramChannelFactory = ChannelFactory<DatagramChannel> { NioDatagramChannel() }
|
||||||
val eventExecutorGroup = run {
|
val serverChannelFactory = ChannelFactory<ServerSocketChannel> { NioServerSocketChannel() }
|
||||||
val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) {
|
val workerGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory())
|
||||||
Thread.ofVirtual().factory()
|
|
||||||
} else {
|
val serverInitializer = ServerInitializer(cfg, channelFactory, datagramChannelFactory)
|
||||||
null
|
|
||||||
}
|
|
||||||
DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory)
|
|
||||||
}
|
|
||||||
val serverInitializer = ServerInitializer(cfg, eventExecutorGroup)
|
|
||||||
val bootstrap = ServerBootstrap().apply {
|
val bootstrap = ServerBootstrap().apply {
|
||||||
// Configure the server
|
// Configure the server
|
||||||
group(bossGroup, workerGroup)
|
group(bossGroup, workerGroup)
|
||||||
channel(serverSocketChannel)
|
channelFactory(serverChannelFactory)
|
||||||
childHandler(serverInitializer)
|
childHandler(serverInitializer)
|
||||||
option(ChannelOption.SO_BACKLOG, cfg.incomingConnectionsBacklogSize)
|
option(ChannelOption.SO_BACKLOG, cfg.incomingConnectionsBacklogSize)
|
||||||
childOption(ChannelOption.SO_KEEPALIVE, true)
|
childOption(ChannelOption.SO_KEEPALIVE, true)
|
||||||
@@ -494,7 +467,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
|
|||||||
return ServerHandle(
|
return ServerHandle(
|
||||||
httpChannel.closeFuture(),
|
httpChannel.closeFuture(),
|
||||||
bossGroup,
|
bossGroup,
|
||||||
setOf(workerGroup, eventExecutorGroup),
|
setOf(workerGroup),
|
||||||
serverInitializer
|
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 {
|
companion object {
|
||||||
private val METHOD_MAP = mapOf(
|
private val METHOD_MAP = mapOf(
|
||||||
Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE),
|
Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD),
|
||||||
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST)
|
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST),
|
||||||
|
Role.Healthcheck to setOf(HttpMethod.TRACE)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
package net.woggioni.rbcs.server.cache
|
package net.woggioni.rbcs.server.cache
|
||||||
|
|
||||||
import net.woggioni.jwo.JWO
|
|
||||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
|
||||||
import net.woggioni.rbcs.common.createLogger
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.ObjectInputStream
|
import java.io.ObjectInputStream
|
||||||
@@ -18,11 +15,16 @@ import java.nio.file.StandardOpenOption
|
|||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
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(
|
class FileSystemCache(
|
||||||
val root: Path,
|
val root: Path,
|
||||||
val maxAge: Duration
|
val maxAge: Duration
|
||||||
) : AutoCloseable {
|
) : AsyncCloseable {
|
||||||
|
|
||||||
class EntryValue(val metadata: CacheValueMetadata, val channel : FileChannel, val offset : Long, val size : Long) : Serializable
|
class EntryValue(val metadata: CacheValueMetadata, val channel : FileChannel, val offset : Long, val size : Long) : Serializable
|
||||||
|
|
||||||
@@ -112,9 +114,18 @@ class FileSystemCache(
|
|||||||
return FileSink(metadata, file, tmpFile)
|
return FileSink(metadata, file, tmpFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val garbageCollector = Thread.ofVirtual().name("file-system-cache-gc").start {
|
private val closeFuture = object : CompletableFuture<Void>() {
|
||||||
while (running) {
|
init {
|
||||||
gc()
|
Thread.ofVirtual().name("file-system-cache-gc").start {
|
||||||
|
try {
|
||||||
|
while (running) {
|
||||||
|
gc()
|
||||||
|
}
|
||||||
|
complete(null)
|
||||||
|
} catch (ex : Throwable) {
|
||||||
|
completeExceptionally(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,8 +162,8 @@ class FileSystemCache(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun asyncClose() : CompletableFuture<Void> {
|
||||||
running = false
|
running = false
|
||||||
garbageCollector.join()
|
return closeFuture
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,11 +1,15 @@
|
|||||||
package net.woggioni.rbcs.server.cache
|
package net.woggioni.rbcs.server.cache
|
||||||
|
|
||||||
|
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.jwo.Application
|
||||||
import net.woggioni.rbcs.api.CacheHandlerFactory
|
import net.woggioni.rbcs.api.CacheHandlerFactory
|
||||||
import net.woggioni.rbcs.api.Configuration
|
import net.woggioni.rbcs.api.Configuration
|
||||||
import net.woggioni.rbcs.common.RBCS
|
import net.woggioni.rbcs.common.RBCS
|
||||||
import java.nio.file.Path
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
data class FileSystemCacheConfiguration(
|
data class FileSystemCacheConfiguration(
|
||||||
val root: Path?,
|
val root: Path?,
|
||||||
@@ -13,17 +17,19 @@ data class FileSystemCacheConfiguration(
|
|||||||
val digestAlgorithm : String?,
|
val digestAlgorithm : String?,
|
||||||
val compressionEnabled: Boolean,
|
val compressionEnabled: Boolean,
|
||||||
val compressionLevel: Int,
|
val compressionLevel: Int,
|
||||||
val chunkSize: Int,
|
|
||||||
) : Configuration.Cache {
|
) : Configuration.Cache {
|
||||||
|
|
||||||
override fun materialize() = object : CacheHandlerFactory {
|
override fun materialize() = object : CacheHandlerFactory {
|
||||||
private val cache = FileSystemCache(root ?: Application.builder("rbcs").build().computeCacheDirectory(), maxAge)
|
private val cache = FileSystemCache(root ?: Application.builder("rbcs").build().computeCacheDirectory(), maxAge)
|
||||||
|
|
||||||
override fun close() {
|
override fun asyncClose() = cache.asyncClose()
|
||||||
cache.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newHandler() = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, chunkSize)
|
override fun newHandler(
|
||||||
|
cfg : Configuration,
|
||||||
|
eventLoop: EventLoopGroup,
|
||||||
|
socketChannelFactory: ChannelFactory<SocketChannel>,
|
||||||
|
datagramChannelFactory: ChannelFactory<DatagramChannel>
|
||||||
|
) = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, cfg.connection.chunkSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI
|
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.buffer.ByteBuf
|
||||||
import io.netty.channel.ChannelHandlerContext
|
import io.netty.channel.ChannelHandlerContext
|
||||||
import io.netty.channel.SimpleChannelInboundHandler
|
|
||||||
import io.netty.handler.codec.http.LastHttpContent
|
import io.netty.handler.codec.http.LastHttpContent
|
||||||
import io.netty.handler.stream.ChunkedNioFile
|
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
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest
|
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.CacheValueNotFoundResponse
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
|
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
|
||||||
import net.woggioni.rbcs.common.RBCS.processCacheKey
|
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(
|
class FileSystemCacheHandler(
|
||||||
private val cache: FileSystemCache,
|
private val cache: FileSystemCache,
|
||||||
@@ -26,12 +26,18 @@ class FileSystemCacheHandler(
|
|||||||
private val compressionEnabled: Boolean,
|
private val compressionEnabled: Boolean,
|
||||||
private val compressionLevel: Int,
|
private val compressionLevel: Int,
|
||||||
private val chunkSize: Int
|
private val chunkSize: Int
|
||||||
) : SimpleChannelInboundHandler<CacheMessage>() {
|
) : CacheHandler() {
|
||||||
|
|
||||||
|
private interface InProgressRequest{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InProgressGetRequest(val request : CacheGetRequest) : InProgressRequest
|
||||||
|
|
||||||
private inner class InProgressPutRequest(
|
private inner class InProgressPutRequest(
|
||||||
val key : String,
|
val key : String,
|
||||||
private val fileSink : FileSystemCache.FileSink
|
private val fileSink : FileSystemCache.FileSink
|
||||||
) {
|
) : InProgressRequest {
|
||||||
|
|
||||||
private val stream = Channels.newOutputStream(fileSink.channel).let {
|
private val stream = Channels.newOutputStream(fileSink.channel).let {
|
||||||
if (compressionEnabled) {
|
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) {
|
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
|
||||||
when (msg) {
|
when (msg) {
|
||||||
@@ -68,55 +74,64 @@ class FileSystemCacheHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
|
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
|
||||||
val key = String(Base64.getUrlEncoder().encode(processCacheKey(msg.key, digestAlgorithm)))
|
inProgressRequest = InProgressGetRequest(msg)
|
||||||
cache.get(key)?.also { entryValue ->
|
|
||||||
ctx.writeAndFlush(CacheValueFoundResponse(msg.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) {
|
|
||||||
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) {
|
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)
|
val sink = cache.put(key, msg.metadata)
|
||||||
inProgressPutRequest = InProgressPutRequest(msg.key, sink)
|
inProgressRequest = InProgressPutRequest(msg.key, sink)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
|
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) {
|
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
|
||||||
inProgressPutRequest?.let { request ->
|
when(val request = inProgressRequest) {
|
||||||
inProgressPutRequest = null
|
is InProgressPutRequest -> {
|
||||||
request.write(msg.content())
|
inProgressRequest = null
|
||||||
request.commit()
|
request.write(msg.content())
|
||||||
ctx.writeAndFlush(CachePutResponse(request.key))
|
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) {
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||||
inProgressPutRequest?.rollback()
|
(inProgressRequest as? InProgressPutRequest)?.rollback()
|
||||||
super.exceptionCaught(ctx, cause)
|
super.exceptionCaught(ctx, cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,18 +1,18 @@
|
|||||||
package net.woggioni.rbcs.server.cache
|
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.api.CacheProvider
|
||||||
import net.woggioni.rbcs.common.RBCS
|
import net.woggioni.rbcs.common.RBCS
|
||||||
import net.woggioni.rbcs.common.Xml
|
import net.woggioni.rbcs.common.Xml
|
||||||
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||||
import org.w3c.dom.Document
|
import org.w3c.dom.Document
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import java.nio.file.Path
|
|
||||||
import java.time.Duration
|
|
||||||
import java.util.zip.Deflater
|
|
||||||
|
|
||||||
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
||||||
|
|
||||||
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd"
|
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs-server.xsd"
|
||||||
|
|
||||||
override fun getXmlType() = "fileSystemCacheType"
|
override fun getXmlType() = "fileSystemCacheType"
|
||||||
|
|
||||||
@@ -30,10 +30,7 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
|||||||
val compressionLevel = el.renderAttribute("compression-level")
|
val compressionLevel = el.renderAttribute("compression-level")
|
||||||
?.let(String::toInt)
|
?.let(String::toInt)
|
||||||
?: Deflater.DEFAULT_COMPRESSION
|
?: Deflater.DEFAULT_COMPRESSION
|
||||||
val digestAlgorithm = el.renderAttribute("digest") ?: "MD5"
|
val digestAlgorithm = el.renderAttribute("digest")
|
||||||
val chunkSize = el.renderAttribute("chunk-size")
|
|
||||||
?.let(Integer::decode)
|
|
||||||
?: 0x10000
|
|
||||||
|
|
||||||
return FileSystemCacheConfiguration(
|
return FileSystemCacheConfiguration(
|
||||||
path,
|
path,
|
||||||
@@ -41,7 +38,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
|||||||
digestAlgorithm,
|
digestAlgorithm,
|
||||||
enableCompression,
|
enableCompression,
|
||||||
compressionLevel,
|
compressionLevel,
|
||||||
chunkSize
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +59,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
|
|||||||
}?.let {
|
}?.let {
|
||||||
attr("compression-level", it.toString())
|
attr("compression-level", it.toString())
|
||||||
}
|
}
|
||||||
attr("chunk-size", chunkSize.toString())
|
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,16 @@
|
|||||||
package net.woggioni.rbcs.server.cache
|
package net.woggioni.rbcs.server.cache
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf
|
import io.netty.buffer.ByteBuf
|
||||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
|
||||||
import net.woggioni.rbcs.common.createLogger
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.PriorityQueue
|
||||||
import java.util.concurrent.PriorityBlockingQueue
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
import net.woggioni.rbcs.api.AsyncCloseable
|
||||||
|
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||||
|
import net.woggioni.rbcs.common.createLogger
|
||||||
|
|
||||||
private class CacheKey(private val value: ByteArray) {
|
private class CacheKey(private val value: ByteArray) {
|
||||||
override fun equals(other: Any?) = if (other is CacheKey) {
|
override fun equals(other: Any?) = if (other is CacheKey) {
|
||||||
@@ -26,47 +28,69 @@ class CacheEntry(
|
|||||||
class InMemoryCache(
|
class InMemoryCache(
|
||||||
private val maxAge: Duration,
|
private val maxAge: Duration,
|
||||||
private val maxSize: Long
|
private val maxSize: Long
|
||||||
) : AutoCloseable {
|
) : AsyncCloseable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = createLogger<InMemoryCache>()
|
private val log = createLogger<InMemoryCache>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val size = AtomicLong()
|
private var mapSize : Long = 0
|
||||||
private val map = ConcurrentHashMap<CacheKey, CacheEntry>()
|
private val map = HashMap<CacheKey, CacheEntry>()
|
||||||
|
private val lock = ReentrantReadWriteLock()
|
||||||
|
private val cond = lock.writeLock().newCondition()
|
||||||
|
|
||||||
private class RemovalQueueElement(val key: CacheKey, val value: CacheEntry, val expiry: Instant) :
|
private class RemovalQueueElement(val key: CacheKey, val value: CacheEntry, val expiry: Instant) :
|
||||||
Comparable<RemovalQueueElement> {
|
Comparable<RemovalQueueElement> {
|
||||||
override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry)
|
override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>()
|
private val removalQueue = PriorityQueue<RemovalQueueElement>()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var running = true
|
private var running = true
|
||||||
|
|
||||||
private val garbageCollector = Thread.ofVirtual().name("in-memory-cache-gc").start {
|
private val closeFuture = object : CompletableFuture<Void>() {
|
||||||
while (running) {
|
init {
|
||||||
val el = removalQueue.poll(1, TimeUnit.SECONDS) ?: continue
|
Thread.ofVirtual().name("in-memory-cache-gc").start {
|
||||||
val value = el.value
|
try {
|
||||||
val now = Instant.now()
|
lock.writeLock().withLock {
|
||||||
if (now > el.expiry) {
|
while (running) {
|
||||||
val removed = map.remove(el.key, value)
|
val el = removalQueue.poll()
|
||||||
if (removed) {
|
if(el == null) {
|
||||||
updateSizeAfterRemoval(value.content)
|
cond.await(1000, TimeUnit.MILLISECONDS)
|
||||||
//Decrease the reference count for map
|
continue
|
||||||
value.content.release()
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.forEach {
|
||||||
|
it.value.content.release()
|
||||||
|
}
|
||||||
|
map.clear()
|
||||||
|
}
|
||||||
|
complete(null)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
completeExceptionally(ex)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
removalQueue.put(el)
|
|
||||||
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeEldest(): Long {
|
fun removeEldest(): Long {
|
||||||
while (true) {
|
while (true) {
|
||||||
val el = removalQueue.take()
|
val el = removalQueue.poll() ?: return mapSize
|
||||||
val value = el.value
|
val value = el.value
|
||||||
val removed = map.remove(el.key, value)
|
val removed = map.remove(el.key, value)
|
||||||
if (removed) {
|
if (removed) {
|
||||||
@@ -79,18 +103,22 @@ class InMemoryCache(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSizeAfterRemoval(removed: ByteBuf): Long {
|
private fun updateSizeAfterRemoval(removed: ByteBuf): Long {
|
||||||
return size.updateAndGet { currentSize: Long ->
|
mapSize -= removed.readableBytes()
|
||||||
currentSize - removed.readableBytes()
|
return mapSize
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun asyncClose() : CompletableFuture<Void> {
|
||||||
running = false
|
running = false
|
||||||
garbageCollector.join()
|
lock.writeLock().withLock {
|
||||||
|
cond.signal()
|
||||||
|
}
|
||||||
|
return closeFuture
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(key: ByteArray) = map[CacheKey(key)]?.run {
|
fun get(key: ByteArray) = lock.readLock().withLock {
|
||||||
CacheEntry(metadata, content.retainedDuplicate())
|
map[CacheKey(key)]?.run {
|
||||||
|
CacheEntry(metadata, content.retainedDuplicate())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(
|
fun put(
|
||||||
@@ -98,18 +126,18 @@ class InMemoryCache(
|
|||||||
value: CacheEntry,
|
value: CacheEntry,
|
||||||
) {
|
) {
|
||||||
val cacheKey = CacheKey(key)
|
val cacheKey = CacheKey(key)
|
||||||
val oldSize = map.put(cacheKey, value)?.let { old ->
|
lock.writeLock().withLock {
|
||||||
val result = old.content.readableBytes()
|
val oldSize = map.put(cacheKey, value)?.let { old ->
|
||||||
old.content.release()
|
val result = old.content.readableBytes()
|
||||||
result
|
old.content.release()
|
||||||
} ?: 0
|
result
|
||||||
val delta = value.content.readableBytes() - oldSize
|
} ?: 0
|
||||||
var newSize = size.updateAndGet { currentSize: Long ->
|
val delta = value.content.readableBytes() - oldSize
|
||||||
currentSize + delta
|
mapSize += delta
|
||||||
}
|
removalQueue.offer(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
|
||||||
removalQueue.put(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
|
while (mapSize > maxSize) {
|
||||||
while (newSize > maxSize) {
|
removeEldest()
|
||||||
newSize = removeEldest()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,9 +1,13 @@
|
|||||||
package net.woggioni.rbcs.server.cache
|
package net.woggioni.rbcs.server.cache
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelFactory
|
||||||
|
import io.netty.channel.EventLoopGroup
|
||||||
|
import io.netty.channel.socket.DatagramChannel
|
||||||
|
import io.netty.channel.socket.SocketChannel
|
||||||
|
import java.time.Duration
|
||||||
import net.woggioni.rbcs.api.CacheHandlerFactory
|
import net.woggioni.rbcs.api.CacheHandlerFactory
|
||||||
import net.woggioni.rbcs.api.Configuration
|
import net.woggioni.rbcs.api.Configuration
|
||||||
import net.woggioni.rbcs.common.RBCS
|
import net.woggioni.rbcs.common.RBCS
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
data class InMemoryCacheConfiguration(
|
data class InMemoryCacheConfiguration(
|
||||||
val maxAge: Duration,
|
val maxAge: Duration,
|
||||||
@@ -11,16 +15,18 @@ data class InMemoryCacheConfiguration(
|
|||||||
val digestAlgorithm : String?,
|
val digestAlgorithm : String?,
|
||||||
val compressionEnabled: Boolean,
|
val compressionEnabled: Boolean,
|
||||||
val compressionLevel: Int,
|
val compressionLevel: Int,
|
||||||
val chunkSize : Int
|
|
||||||
) : Configuration.Cache {
|
) : Configuration.Cache {
|
||||||
override fun materialize() = object : CacheHandlerFactory {
|
override fun materialize() = object : CacheHandlerFactory {
|
||||||
private val cache = InMemoryCache(maxAge, maxSize)
|
private val cache = InMemoryCache(maxAge, maxSize)
|
||||||
|
|
||||||
override fun close() {
|
override fun asyncClose() = cache.asyncClose()
|
||||||
cache.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newHandler() = InMemoryCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel)
|
override fun newHandler(
|
||||||
|
cfg : Configuration,
|
||||||
|
eventLoop: EventLoopGroup,
|
||||||
|
socketChannelFactory: ChannelFactory<SocketChannel>,
|
||||||
|
datagramChannelFactory: ChannelFactory<DatagramChannel>
|
||||||
|
) = InMemoryCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI
|
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI
|
||||||
|
@@ -2,7 +2,10 @@ package net.woggioni.rbcs.server.cache
|
|||||||
|
|
||||||
import io.netty.buffer.ByteBuf
|
import io.netty.buffer.ByteBuf
|
||||||
import io.netty.channel.ChannelHandlerContext
|
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
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest
|
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.api.message.CacheMessage.LastCacheContent
|
||||||
import net.woggioni.rbcs.common.ByteBufOutputStream
|
import net.woggioni.rbcs.common.ByteBufOutputStream
|
||||||
import net.woggioni.rbcs.common.RBCS.processCacheKey
|
import net.woggioni.rbcs.common.RBCS.processCacheKey
|
||||||
import java.util.zip.Deflater
|
|
||||||
import java.util.zip.DeflaterOutputStream
|
|
||||||
import java.util.zip.InflaterOutputStream
|
|
||||||
|
|
||||||
class InMemoryCacheHandler(
|
class InMemoryCacheHandler(
|
||||||
private val cache: InMemoryCache,
|
private val cache: InMemoryCache,
|
||||||
private val digestAlgorithm: String?,
|
private val digestAlgorithm: String?,
|
||||||
private val compressionEnabled: Boolean,
|
private val compressionEnabled: Boolean,
|
||||||
private val compressionLevel: Int
|
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 request: CachePutRequest
|
||||||
val buf: ByteBuf
|
val buf: ByteBuf
|
||||||
|
|
||||||
@@ -33,18 +41,14 @@ class InMemoryCacheHandler(
|
|||||||
|
|
||||||
private inner class InProgressPlainPutRequest(ctx: ChannelHandlerContext, override val request: CachePutRequest) :
|
private inner class InProgressPlainPutRequest(ctx: ChannelHandlerContext, override val request: CachePutRequest) :
|
||||||
InProgressPutRequest {
|
InProgressPutRequest {
|
||||||
override val buf = ctx.alloc().compositeBuffer()
|
override val buf = ctx.alloc().compositeHeapBuffer()
|
||||||
|
|
||||||
private val stream = ByteBufOutputStream(buf).let {
|
|
||||||
if (compressionEnabled) {
|
|
||||||
DeflaterOutputStream(it, Deflater(compressionLevel))
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun append(buf: ByteBuf) {
|
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() {
|
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) {
|
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
|
||||||
when (msg) {
|
when (msg) {
|
||||||
@@ -85,23 +89,11 @@ class InMemoryCacheHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
|
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
|
||||||
cache.get(processCacheKey(msg.key, digestAlgorithm))?.let { value ->
|
inProgressRequest = InProgressGetRequest(msg)
|
||||||
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())
|
|
||||||
buf.retain()
|
|
||||||
}
|
|
||||||
ctx.writeAndFlush(LastCacheContent(buf))
|
|
||||||
} else {
|
|
||||||
ctx.writeAndFlush(LastCacheContent(value.content))
|
|
||||||
}
|
|
||||||
} ?: ctx.writeAndFlush(CacheValueNotFoundResponse())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
|
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
|
||||||
inProgressPutRequest = if(compressionEnabled) {
|
inProgressRequest = if (compressionEnabled) {
|
||||||
InProgressCompressedPutRequest(ctx, msg)
|
InProgressCompressedPutRequest(ctx, msg)
|
||||||
} else {
|
} else {
|
||||||
InProgressPlainPutRequest(ctx, msg)
|
InProgressPlainPutRequest(ctx, msg)
|
||||||
@@ -109,27 +101,47 @@ class InMemoryCacheHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
|
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) {
|
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
|
||||||
handleCacheContent(ctx, msg)
|
handleCacheContent(ctx, msg)
|
||||||
inProgressPutRequest?.let { inProgressRequest ->
|
when (val req = inProgressRequest) {
|
||||||
inProgressPutRequest = null
|
is InProgressGetRequest -> {
|
||||||
val buf = inProgressRequest.buf
|
cache.get(processCacheKey(req.request.key, null, digestAlgorithm))?.let { value ->
|
||||||
buf.retain()
|
sendMessageAndFlush(ctx, CacheValueFoundResponse(req.request.key, value.metadata))
|
||||||
inProgressRequest.close()
|
if (compressionEnabled) {
|
||||||
val cacheKey = processCacheKey(inProgressRequest.request.key, digestAlgorithm)
|
val buf = ctx.alloc().heapBuffer()
|
||||||
cache.put(cacheKey, CacheEntry(inProgressRequest.request.metadata, buf))
|
InflaterOutputStream(ByteBufOutputStream(buf)).use {
|
||||||
ctx.writeAndFlush(CachePutResponse(inProgressRequest.request.key))
|
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) {
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||||
inProgressPutRequest?.let { req ->
|
inProgressRequest?.close()
|
||||||
req.buf.release()
|
inProgressRequest = null
|
||||||
inProgressPutRequest = null
|
|
||||||
}
|
|
||||||
super.exceptionCaught(ctx, cause)
|
super.exceptionCaught(ctx, cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,17 +1,17 @@
|
|||||||
package net.woggioni.rbcs.server.cache
|
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.api.CacheProvider
|
||||||
import net.woggioni.rbcs.common.RBCS
|
import net.woggioni.rbcs.common.RBCS
|
||||||
import net.woggioni.rbcs.common.Xml
|
import net.woggioni.rbcs.common.Xml
|
||||||
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||||
import org.w3c.dom.Document
|
import org.w3c.dom.Document
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import java.time.Duration
|
|
||||||
import java.util.zip.Deflater
|
|
||||||
|
|
||||||
class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
||||||
|
|
||||||
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs.xsd"
|
override fun getXmlSchemaLocation() = "classpath:net/woggioni/rbcs/server/schema/rbcs-server.xsd"
|
||||||
|
|
||||||
override fun getXmlType() = "inMemoryCacheType"
|
override fun getXmlType() = "inMemoryCacheType"
|
||||||
|
|
||||||
@@ -30,17 +30,13 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
|||||||
val compressionLevel = el.renderAttribute("compression-level")
|
val compressionLevel = el.renderAttribute("compression-level")
|
||||||
?.let(String::toInt)
|
?.let(String::toInt)
|
||||||
?: Deflater.DEFAULT_COMPRESSION
|
?: Deflater.DEFAULT_COMPRESSION
|
||||||
val digestAlgorithm = el.renderAttribute("digest") ?: "MD5"
|
val digestAlgorithm = el.renderAttribute("digest")
|
||||||
val chunkSize = el.renderAttribute("chunk-size")
|
|
||||||
?.let(Integer::decode)
|
|
||||||
?: 0x10000
|
|
||||||
return InMemoryCacheConfiguration(
|
return InMemoryCacheConfiguration(
|
||||||
maxAge,
|
maxAge,
|
||||||
maxSize,
|
maxSize,
|
||||||
digestAlgorithm,
|
digestAlgorithm,
|
||||||
enableCompression,
|
enableCompression,
|
||||||
compressionLevel,
|
compressionLevel,
|
||||||
chunkSize
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +56,6 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
|
|||||||
}?.let {
|
}?.let {
|
||||||
attr("compression-level", it.toString())
|
attr("compression-level", it.toString())
|
||||||
}
|
}
|
||||||
attr("chunk-size", chunkSize.toString())
|
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package net.woggioni.rbcs.server.configuration
|
package net.woggioni.rbcs.server.configuration
|
||||||
|
|
||||||
|
import java.util.ServiceLoader
|
||||||
import net.woggioni.rbcs.api.CacheProvider
|
import net.woggioni.rbcs.api.CacheProvider
|
||||||
import net.woggioni.rbcs.api.Configuration
|
import net.woggioni.rbcs.api.Configuration
|
||||||
import java.util.ServiceLoader
|
|
||||||
|
|
||||||
object CacheSerializers {
|
object CacheSerializers {
|
||||||
val index = (Configuration::class.java.module.layer?.let { layer ->
|
val index = (Configuration::class.java.module.layer?.let { layer ->
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
package net.woggioni.rbcs.server.configuration
|
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
|
||||||
import net.woggioni.rbcs.api.Configuration.Authentication
|
import net.woggioni.rbcs.api.Configuration.Authentication
|
||||||
import net.woggioni.rbcs.api.Configuration.BasicAuthentication
|
import net.woggioni.rbcs.api.Configuration.BasicAuthentication
|
||||||
@@ -18,22 +21,19 @@ import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
|||||||
import org.w3c.dom.Document
|
import org.w3c.dom.Document
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import org.w3c.dom.TypeInfo
|
import org.w3c.dom.TypeInfo
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
|
|
||||||
object Parser {
|
object Parser {
|
||||||
fun parse(document: Document): Configuration {
|
fun parse(document: Document): Configuration {
|
||||||
val root = document.documentElement
|
val root = document.documentElement
|
||||||
val anonymousUser = User("", null, emptySet(), null)
|
val anonymousUser = User("", null, emptySet(), null)
|
||||||
var connection: Configuration.Connection = Configuration.Connection(
|
var connection: Configuration.Connection = Configuration.Connection(
|
||||||
Duration.of(10, ChronoUnit.SECONDS),
|
Duration.of(30, ChronoUnit.SECONDS),
|
||||||
Duration.of(10, ChronoUnit.SECONDS),
|
|
||||||
Duration.of(60, ChronoUnit.SECONDS),
|
Duration.of(60, ChronoUnit.SECONDS),
|
||||||
Duration.of(30, ChronoUnit.SECONDS),
|
Duration.of(60, ChronoUnit.SECONDS),
|
||||||
Duration.of(30, ChronoUnit.SECONDS),
|
0x4000000,
|
||||||
67108864
|
0x10000
|
||||||
)
|
)
|
||||||
|
var rateLimiter = Configuration.RateLimiter(false, 0x100000, 100)
|
||||||
var eventExecutor: Configuration.EventExecutor = Configuration.EventExecutor(true)
|
var eventExecutor: Configuration.EventExecutor = Configuration.EventExecutor(true)
|
||||||
var cache: Cache? = null
|
var cache: Cache? = null
|
||||||
var host = "127.0.0.1"
|
var host = "127.0.0.1"
|
||||||
@@ -113,10 +113,6 @@ object Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"connection" -> {
|
"connection" -> {
|
||||||
val writeTimeout = child.renderAttribute("write-timeout")
|
|
||||||
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
|
|
||||||
val readTimeout = child.renderAttribute("read-timeout")
|
|
||||||
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
|
|
||||||
val idleTimeout = child.renderAttribute("idle-timeout")
|
val idleTimeout = child.renderAttribute("idle-timeout")
|
||||||
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
|
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
|
||||||
val readIdleTimeout = child.renderAttribute("read-idle-timeout")
|
val readIdleTimeout = child.renderAttribute("read-idle-timeout")
|
||||||
@@ -125,22 +121,36 @@ object Parser {
|
|||||||
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
||||||
val maxRequestSize = child.renderAttribute("max-request-size")
|
val maxRequestSize = child.renderAttribute("max-request-size")
|
||||||
?.let(Integer::decode) ?: 0x4000000
|
?.let(Integer::decode) ?: 0x4000000
|
||||||
|
val chunkSize = child.renderAttribute("chunk-size")
|
||||||
|
?.let(Integer::decode) ?: 0x10000
|
||||||
connection = Configuration.Connection(
|
connection = Configuration.Connection(
|
||||||
readTimeout,
|
|
||||||
writeTimeout,
|
|
||||||
idleTimeout,
|
idleTimeout,
|
||||||
readIdleTimeout,
|
readIdleTimeout,
|
||||||
writeIdleTimeout,
|
writeIdleTimeout,
|
||||||
maxRequestSize
|
maxRequestSize,
|
||||||
|
chunkSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"event-executor" -> {
|
"event-executor" -> {
|
||||||
val useVirtualThread = root.renderAttribute("use-virtual-threads")
|
val useVirtualThread = child.renderAttribute("use-virtual-threads")
|
||||||
?.let(String::toBoolean) ?: true
|
?.let(String::toBoolean) ?: true
|
||||||
eventExecutor = Configuration.EventExecutor(useVirtualThread)
|
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" -> {
|
"tls" -> {
|
||||||
var keyStore: KeyStore? = null
|
var keyStore: KeyStore? = null
|
||||||
var trustStore: TrustStore? = null
|
var trustStore: TrustStore? = null
|
||||||
@@ -188,6 +198,7 @@ object Parser {
|
|||||||
incomingConnectionsBacklogSize,
|
incomingConnectionsBacklogSize,
|
||||||
serverPath,
|
serverPath,
|
||||||
eventExecutor,
|
eventExecutor,
|
||||||
|
rateLimiter,
|
||||||
connection,
|
connection,
|
||||||
users,
|
users,
|
||||||
groups,
|
groups,
|
||||||
@@ -201,6 +212,7 @@ object Parser {
|
|||||||
when (it.localName) {
|
when (it.localName) {
|
||||||
"reader" -> Role.Reader
|
"reader" -> Role.Reader
|
||||||
"writer" -> Role.Writer
|
"writer" -> Role.Writer
|
||||||
|
"healthcheck" -> Role.Healthcheck
|
||||||
else -> throw UnsupportedOperationException("Illegal node '${it.localName}'")
|
else -> throw UnsupportedOperationException("Illegal node '${it.localName}'")
|
||||||
}
|
}
|
||||||
}.toSet()
|
}.toSet()
|
||||||
|
@@ -36,17 +36,21 @@ object Serializer {
|
|||||||
}
|
}
|
||||||
node("connection") {
|
node("connection") {
|
||||||
conf.connection.let { connection ->
|
conf.connection.let { connection ->
|
||||||
attr("read-timeout", connection.readTimeout.toString())
|
|
||||||
attr("write-timeout", connection.writeTimeout.toString())
|
|
||||||
attr("idle-timeout", connection.idleTimeout.toString())
|
attr("idle-timeout", connection.idleTimeout.toString())
|
||||||
attr("read-idle-timeout", connection.readIdleTimeout.toString())
|
attr("read-idle-timeout", connection.readIdleTimeout.toString())
|
||||||
attr("write-idle-timeout", connection.writeIdleTimeout.toString())
|
attr("write-idle-timeout", connection.writeIdleTimeout.toString())
|
||||||
attr("max-request-size", connection.maxRequestSize.toString())
|
attr("max-request-size", connection.maxRequestSize.toString())
|
||||||
|
attr("chunk-size", connection.chunkSize.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node("event-executor") {
|
node("event-executor") {
|
||||||
attr("use-virtual-threads", conf.eventExecutor.isUseVirtualThreads.toString())
|
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 cache = conf.cache
|
||||||
val serializer : CacheProvider<Configuration.Cache> =
|
val serializer : CacheProvider<Configuration.Cache> =
|
||||||
(CacheSerializers.index[cache.namespaceURI to cache.typeName] as? CacheProvider<Configuration.Cache>) ?: throw NotImplementedError()
|
(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.codec.http.HttpVersion
|
||||||
import io.netty.handler.timeout.ReadTimeoutException
|
import io.netty.handler.timeout.ReadTimeoutException
|
||||||
import io.netty.handler.timeout.WriteTimeoutException
|
import io.netty.handler.timeout.WriteTimeoutException
|
||||||
|
import 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.CacheException
|
||||||
import net.woggioni.rbcs.api.exception.ContentTooLargeException
|
import net.woggioni.rbcs.api.exception.ContentTooLargeException
|
||||||
import net.woggioni.rbcs.common.contextLogger
|
import net.woggioni.rbcs.common.contextLogger
|
||||||
@@ -20,13 +24,12 @@ import net.woggioni.rbcs.common.debug
|
|||||||
import net.woggioni.rbcs.common.log
|
import net.woggioni.rbcs.common.log
|
||||||
import org.slf4j.event.Level
|
import org.slf4j.event.Level
|
||||||
import org.slf4j.spi.LoggingEventBuilder
|
import org.slf4j.spi.LoggingEventBuilder
|
||||||
import java.net.ConnectException
|
|
||||||
import java.net.SocketException
|
|
||||||
import javax.net.ssl.SSLException
|
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException
|
|
||||||
|
|
||||||
@Sharable
|
@Sharable
|
||||||
object ExceptionHandler : ChannelDuplexHandler() {
|
object ExceptionHandler : ChannelDuplexHandler() {
|
||||||
|
|
||||||
|
val NAME : String = this::class.java.name
|
||||||
|
|
||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
|
|
||||||
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(
|
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
|
package net.woggioni.rbcs.server.handler
|
||||||
|
|
||||||
import io.netty.channel.ChannelDuplexHandler
|
import io.netty.channel.ChannelDuplexHandler
|
||||||
|
import io.netty.channel.ChannelHandler
|
||||||
import io.netty.channel.ChannelHandlerContext
|
import io.netty.channel.ChannelHandlerContext
|
||||||
import io.netty.channel.ChannelPromise
|
import io.netty.channel.ChannelPromise
|
||||||
import io.netty.handler.codec.http.DefaultFullHttpResponse
|
import io.netty.handler.codec.http.DefaultFullHttpResponse
|
||||||
import io.netty.handler.codec.http.DefaultHttpContent
|
import io.netty.handler.codec.http.DefaultHttpContent
|
||||||
import io.netty.handler.codec.http.DefaultHttpResponse
|
import io.netty.handler.codec.http.DefaultHttpResponse
|
||||||
import io.netty.handler.codec.http.DefaultLastHttpContent
|
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.HttpHeaderNames
|
||||||
import io.netty.handler.codec.http.HttpHeaderValues
|
import io.netty.handler.codec.http.HttpHeaderValues
|
||||||
import io.netty.handler.codec.http.HttpHeaders
|
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.HttpResponseStatus
|
||||||
import io.netty.handler.codec.http.HttpUtil
|
import io.netty.handler.codec.http.HttpUtil
|
||||||
import io.netty.handler.codec.http.HttpVersion
|
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.CacheValueMetadata
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage
|
import net.woggioni.rbcs.api.message.CacheMessage
|
||||||
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
|
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.createLogger
|
||||||
import net.woggioni.rbcs.common.debug
|
import net.woggioni.rbcs.common.debug
|
||||||
import net.woggioni.rbcs.common.warn
|
import net.woggioni.rbcs.common.warn
|
||||||
import java.nio.file.Path
|
import net.woggioni.rbcs.server.exception.ExceptionHandler
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class ServerHandler(private val serverPrefix: Path) :
|
class ServerHandler(private val serverPrefix: Path, private val cacheHandlerSupplier : () -> ChannelHandler) :
|
||||||
ChannelDuplexHandler() {
|
ChannelDuplexHandler() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = createLogger<ServerHandler>()
|
private val log = createLogger<ServerHandler>()
|
||||||
val NAME = this::class.java.name
|
val NAME = ServerHandler::class.java.name
|
||||||
}
|
}
|
||||||
|
|
||||||
private var httpVersion = HttpVersion.HTTP_1_1
|
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) {
|
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||||
when (msg) {
|
when (msg) {
|
||||||
is HttpRequest -> handleRequest(ctx, 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)
|
else -> super.channelRead(ctx, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise?) {
|
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise?) {
|
||||||
if (msg is CacheMessage) {
|
if (msg is CacheMessage) {
|
||||||
try {
|
try {
|
||||||
when (msg) {
|
when (msg) {
|
||||||
is CachePutResponse -> {
|
is CachePutResponse -> {
|
||||||
|
log.debug(ctx) {
|
||||||
|
"Added value for key '${msg.key}' to build cache"
|
||||||
|
}
|
||||||
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.CREATED)
|
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.CREATED)
|
||||||
val keyBytes = msg.key.toByteArray(Charsets.UTF_8)
|
val keyBytes = msg.key.toByteArray(Charsets.UTF_8)
|
||||||
response.headers().apply {
|
response.headers().apply {
|
||||||
@@ -88,6 +107,9 @@ class ServerHandler(private val serverPrefix: Path) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
is CacheValueNotFoundResponse -> {
|
is CacheValueNotFoundResponse -> {
|
||||||
|
log.debug(ctx) {
|
||||||
|
"Value not found for key '${msg.key}'"
|
||||||
|
}
|
||||||
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.NOT_FOUND)
|
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.NOT_FOUND)
|
||||||
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
|
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
|
||||||
setKeepAliveHeader(response.headers())
|
setKeepAliveHeader(response.headers())
|
||||||
@@ -95,6 +117,9 @@ class ServerHandler(private val serverPrefix: Path) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
is CacheValueFoundResponse -> {
|
is CacheValueFoundResponse -> {
|
||||||
|
log.debug(ctx) {
|
||||||
|
"Retrieved value for key '${msg.key}'"
|
||||||
|
}
|
||||||
val response = DefaultHttpResponse(httpVersion, HttpResponseStatus.OK)
|
val response = DefaultHttpResponse(httpVersion, HttpResponseStatus.OK)
|
||||||
response.headers().apply {
|
response.headers().apply {
|
||||||
set(HttpHeaderNames.CONTENT_TYPE, msg.metadata.mimeType ?: HttpHeaderValues.APPLICATION_OCTET_STREAM)
|
set(HttpHeaderNames.CONTENT_TYPE, msg.metadata.mimeType ?: HttpHeaderValues.APPLICATION_OCTET_STREAM)
|
||||||
@@ -127,6 +152,8 @@ class ServerHandler(private val serverPrefix: Path) :
|
|||||||
} finally {
|
} finally {
|
||||||
resetRequestMetadata()
|
resetRequestMetadata()
|
||||||
}
|
}
|
||||||
|
} else if(msg is LastHttpContent) {
|
||||||
|
ctx.write(msg, promise)
|
||||||
} else super.write(ctx, msg, promise)
|
} else super.write(ctx, msg, promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,15 +162,18 @@ class ServerHandler(private val serverPrefix: Path) :
|
|||||||
setRequestMetadata(msg)
|
setRequestMetadata(msg)
|
||||||
val method = msg.method()
|
val method = msg.method()
|
||||||
if (method === HttpMethod.GET) {
|
if (method === HttpMethod.GET) {
|
||||||
val path = Path.of(msg.uri())
|
val path = Path.of(msg.uri()).normalize()
|
||||||
val prefix = path.parent
|
if (path.startsWith(serverPrefix)) {
|
||||||
if (serverPrefix == prefix) {
|
cacheRequestInProgress = true
|
||||||
ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler)
|
val relativePath = serverPrefix.relativize(path)
|
||||||
path.fileName?.toString()
|
val key : String = relativePath.toString()
|
||||||
?.let(::CacheGetRequest)
|
val cacheHandler = cacheHandlerSupplier()
|
||||||
?.let(ctx::fireChannelRead)
|
ctx.pipeline().addBefore(ExceptionHandler.NAME, null, cacheHandler)
|
||||||
?: ctx.channel().write(CacheValueNotFoundResponse())
|
key.let(::CacheGetRequest)
|
||||||
|
.let(ctx::fireChannelRead)
|
||||||
|
?: ctx.channel().write(CacheValueNotFoundResponse(key))
|
||||||
} else {
|
} else {
|
||||||
|
cacheRequestInProgress = false
|
||||||
log.warn(ctx) {
|
log.warn(ctx) {
|
||||||
"Got request for unhandled path '${msg.uri()}'"
|
"Got request for unhandled path '${msg.uri()}'"
|
||||||
}
|
}
|
||||||
@@ -152,23 +182,23 @@ class ServerHandler(private val serverPrefix: Path) :
|
|||||||
ctx.writeAndFlush(response)
|
ctx.writeAndFlush(response)
|
||||||
}
|
}
|
||||||
} else if (method === HttpMethod.PUT) {
|
} else if (method === HttpMethod.PUT) {
|
||||||
val path = Path.of(msg.uri())
|
val path = Path.of(msg.uri()).normalize()
|
||||||
val prefix = path.parent
|
if (path.startsWith(serverPrefix)) {
|
||||||
val key = path.fileName.toString()
|
cacheRequestInProgress = true
|
||||||
|
val relativePath = serverPrefix.relativize(path)
|
||||||
|
val key = relativePath.toString()
|
||||||
|
val cacheHandler = cacheHandlerSupplier()
|
||||||
|
ctx.pipeline().addAfter(NAME, null, cacheHandler)
|
||||||
|
|
||||||
if (serverPrefix == prefix) {
|
|
||||||
log.debug(ctx) {
|
|
||||||
"Added value for key '$key' to build cache"
|
|
||||||
}
|
|
||||||
ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler)
|
|
||||||
path.fileName?.toString()
|
path.fileName?.toString()
|
||||||
?.let {
|
?.let {
|
||||||
val mimeType = HttpUtil.getMimeType(msg)?.toString()
|
val mimeType = HttpUtil.getMimeType(msg)?.toString()
|
||||||
CachePutRequest(key, CacheValueMetadata(msg.headers().get(HttpHeaderNames.CONTENT_DISPOSITION), mimeType))
|
CachePutRequest(key, CacheValueMetadata(msg.headers().get(HttpHeaderNames.CONTENT_DISPOSITION), mimeType))
|
||||||
}
|
}
|
||||||
?.let(ctx::fireChannelRead)
|
?.let(ctx::fireChannelRead)
|
||||||
?: ctx.channel().write(CacheValueNotFoundResponse())
|
?: ctx.channel().write(CacheValueNotFoundResponse(key))
|
||||||
} else {
|
} else {
|
||||||
|
cacheRequestInProgress = false
|
||||||
log.warn(ctx) {
|
log.warn(ctx) {
|
||||||
"Got request for unhandled path '${msg.uri()}'"
|
"Got request for unhandled path '${msg.uri()}'"
|
||||||
}
|
}
|
||||||
@@ -177,8 +207,11 @@ class ServerHandler(private val serverPrefix: Path) :
|
|||||||
ctx.writeAndFlush(response)
|
ctx.writeAndFlush(response)
|
||||||
}
|
}
|
||||||
} else if (method == HttpMethod.TRACE) {
|
} else if (method == HttpMethod.TRACE) {
|
||||||
|
cacheRequestInProgress = false
|
||||||
|
ctx.pipeline().addAfter(NAME, null, TraceHandler)
|
||||||
super.channelRead(ctx, msg)
|
super.channelRead(ctx, msg)
|
||||||
} else {
|
} else {
|
||||||
|
cacheRequestInProgress = false
|
||||||
log.warn(ctx) {
|
log.warn(ctx) {
|
||||||
"Got request with unhandled method '${msg.method().name()}'"
|
"Got request with unhandled method '${msg.method().name()}'"
|
||||||
}
|
}
|
||||||
@@ -188,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) {
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||||
super.exceptionCaught(ctx, cause)
|
super.exceptionCaught(ctx, cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -42,6 +42,7 @@ object TraceHandler : ChannelInboundHandlerAdapter() {
|
|||||||
}
|
}
|
||||||
is LastHttpContent -> {
|
is LastHttpContent -> {
|
||||||
ctx.writeAndFlush(msg)
|
ctx.writeAndFlush(msg)
|
||||||
|
ctx.pipeline().remove(this)
|
||||||
}
|
}
|
||||||
is HttpContent -> ctx.writeAndFlush(msg)
|
is HttpContent -> ctx.writeAndFlush(msg)
|
||||||
else -> super.channelRead(ctx, msg)
|
else -> super.channelRead(ctx, msg)
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
package net.woggioni.rbcs.server.throttling
|
package net.woggioni.rbcs.server.throttling
|
||||||
|
|
||||||
import net.woggioni.jwo.Bucket
|
|
||||||
import net.woggioni.rbcs.api.Configuration
|
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.util.Arrays
|
import java.util.Arrays
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
import net.woggioni.jwo.Bucket
|
||||||
|
import net.woggioni.rbcs.api.Configuration
|
||||||
|
|
||||||
class BucketManager private constructor(
|
class BucketManager private constructor(
|
||||||
private val bucketsByUser: Map<Configuration.User, List<Bucket>> = HashMap(),
|
private val bucketsByUser: Map<Configuration.User, List<Bucket>> = HashMap(),
|
||||||
|
@@ -1,32 +1,50 @@
|
|||||||
package net.woggioni.rbcs.server.throttling
|
package net.woggioni.rbcs.server.throttling
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBufHolder
|
||||||
import io.netty.channel.ChannelHandlerContext
|
import io.netty.channel.ChannelHandlerContext
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||||
import io.netty.handler.codec.http.DefaultFullHttpResponse
|
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.HttpHeaderNames
|
||||||
import io.netty.handler.codec.http.HttpRequest
|
import io.netty.handler.codec.http.HttpRequest
|
||||||
import io.netty.handler.codec.http.HttpResponseStatus
|
import io.netty.handler.codec.http.HttpResponseStatus
|
||||||
import io.netty.handler.codec.http.HttpVersion
|
import io.netty.handler.codec.http.HttpVersion
|
||||||
|
import io.netty.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.Bucket
|
||||||
import net.woggioni.jwo.LongMath
|
import net.woggioni.jwo.LongMath
|
||||||
import net.woggioni.rbcs.api.Configuration
|
import net.woggioni.rbcs.api.Configuration
|
||||||
import net.woggioni.rbcs.common.createLogger
|
import net.woggioni.rbcs.common.createLogger
|
||||||
|
import net.woggioni.rbcs.common.debug
|
||||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer
|
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(
|
||||||
class ThrottlingHandler(private val bucketManager : BucketManager,
|
private val bucketManager: BucketManager,
|
||||||
private val connectionConfiguration : Configuration.Connection) : ChannelInboundHandlerAdapter() {
|
rateLimiterConfiguration: Configuration.RateLimiter,
|
||||||
|
connectionConfiguration: Configuration.Connection
|
||||||
|
) : ChannelInboundHandlerAdapter() {
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private val log = createLogger<ThrottlingHandler>()
|
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
|
* If the suggested waiting time from the bucket is lower than this
|
||||||
@@ -39,38 +57,149 @@ class ThrottlingHandler(private val bucketManager : BucketManager,
|
|||||||
connectionConfiguration.writeIdleTimeout
|
connectionConfiguration.writeIdleTimeout
|
||||||
).dividedBy(2)
|
).dividedBy(2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||||
if(msg is HttpRequest) {
|
if(valveClosed) {
|
||||||
val buckets = mutableListOf<Bucket>()
|
if(msg !is HttpRequest && msg is ByteBufHolder) {
|
||||||
val user = ctx.channel().attr(RemoteBuildCacheServer.userAttribute).get()
|
val newBufferSize = requestBufferSize + msg.content().readableBytes()
|
||||||
if (user != null) {
|
if(newBufferSize > maxMessageBufferSize || queuedContent.size + 1 > maxQueuedMessages) {
|
||||||
bucketManager.getBucketByUser(user)?.let(buckets::addAll)
|
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()
|
} else {
|
||||||
if (groups.isNotEmpty()) {
|
entryPoint(ctx, msg)
|
||||||
groups.forEach { group ->
|
}
|
||||||
bucketManager.getBucketByGroup(group)?.let(buckets::add)
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
super.channelRead(ctx, msg)
|
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
|
var nextAttempt = -1L
|
||||||
for (bucket in buckets) {
|
for (bucket in buckets) {
|
||||||
val bucketNextAttempt = bucket.removeTokensWithEstimate(1)
|
val bucketNextAttempt = bucket.removeTokensWithEstimate(1)
|
||||||
@@ -78,38 +207,19 @@ class ThrottlingHandler(private val bucketManager : BucketManager,
|
|||||||
nextAttempt = bucketNextAttempt
|
nextAttempt = bucketNextAttempt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nextAttempt < 0) {
|
return nextAttempt
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration) {
|
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration?) {
|
||||||
val response = DefaultFullHttpResponse(
|
val response = DefaultFullHttpResponse(
|
||||||
HttpVersion.HTTP_1_1,
|
HttpVersion.HTTP_1_1,
|
||||||
HttpResponseStatus.TOO_MANY_REQUESTS
|
HttpResponseStatus.TOO_MANY_REQUESTS
|
||||||
)
|
)
|
||||||
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
|
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
|
||||||
retryAfter.seconds.takeIf {
|
retryAfter?.seconds?.takeIf {
|
||||||
it > 0
|
it > 0
|
||||||
}?.let {
|
}?.let {
|
||||||
response.headers()[HttpHeaderNames.RETRY_AFTER] = retryAfter.seconds
|
response.headers()[HttpHeaderNames.RETRY_AFTER] = it
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.writeAndFlush(response)
|
ctx.writeAndFlush(response)
|
||||||
|
@@ -2,18 +2,7 @@
|
|||||||
<rbcs:server
|
<rbcs:server
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xmlns:rbcs="urn:net.woggioni.rbcs.server"
|
xmlns: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"/>
|
<bind host="127.0.0.1" port="8080" incoming-connections-backlog-size="1024"/>
|
||||||
<connection
|
<cache xs:type="rbcs:fileSystemCacheType" path="${sys:java.io.tmpdir}/rbcs" max-age="P7D"/>
|
||||||
max-request-size="67108864"
|
|
||||||
idle-timeout="PT30S"
|
|
||||||
read-timeout="PT10S"
|
|
||||||
write-timeout="PT10S"
|
|
||||||
read-idle-timeout="PT60S"
|
|
||||||
write-idle-timeout="PT60S"/>
|
|
||||||
<event-executor use-virtual-threads="true"/>
|
|
||||||
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/>
|
|
||||||
<authentication>
|
|
||||||
<none/>
|
|
||||||
</authentication>
|
|
||||||
</rbcs:server>
|
</rbcs:server>
|
@@ -0,0 +1,684 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<xs:schema targetNamespace="urn:net.woggioni.rbcs.server"
|
||||||
|
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||||
|
xmlns:rbcs="urn:net.woggioni.rbcs.server"
|
||||||
|
elementFormDefault="unqualified">
|
||||||
|
<xs:element name="server" type="rbcs:serverType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Root element containing the server configuration
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:complexType name="serverType">
|
||||||
|
<xs:sequence minOccurs="0">
|
||||||
|
<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>
|
||||||
|
Cache storage backend implementation to use, more implementations can be added through
|
||||||
|
the use of plugins
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="authorization" type="rbcs:authorizationType" minOccurs="0">
|
||||||
|
<xs:key name="userId">
|
||||||
|
<xs:selector xpath="users/user"/>
|
||||||
|
<xs:field xpath="@name"/>
|
||||||
|
</xs:key>
|
||||||
|
<xs:keyref name="userRef" refer="rbcs:userId">
|
||||||
|
<xs:selector xpath="groups/group/users/user"/>
|
||||||
|
<xs:field xpath="@ref"/>
|
||||||
|
</xs:keyref>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="authentication" type="rbcs:authenticationType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Mechanism to use to assign a username to a specific client
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="tls" type="rbcs:tlsType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Use TLS to encrypt all the communications
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="path" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
URI path prefix, if your rbcs is hosted at "http://www.example.com"
|
||||||
|
and this parameter is set to "cache", then all the requests will need to be sent at
|
||||||
|
"http://www.example.com/cache/KEY", where "KEY" is the cache entry KEY
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="bindType">
|
||||||
|
<xs:attribute name="host" type="xs:token" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Server bind address</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="port" type="xs:unsignedShort" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Server port number</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The maximum queue length for incoming connection indications (a request to connect) is set to
|
||||||
|
the backlog parameter. If a connection indication arrives when the queue is full,
|
||||||
|
the connection is refused.
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="connectionType">
|
||||||
|
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The server will close the connection with the client
|
||||||
|
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 server will close the connection with the client
|
||||||
|
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 server will close the connection with the client
|
||||||
|
when no write was performed for the specified period of time.
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="max-request-size" type="rbcs:byteSizeType" use="optional" default="0x4000000">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The maximum request body size the server will accept from a client
|
||||||
|
(if exceeded the server returns 413 HTTP status code)
|
||||||
|
</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">
|
||||||
|
<xs:attribute name="use-virtual-threads" type="xs:boolean" use="optional" default="true">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Whether or not to use virtual threads for the execution of the core server handler
|
||||||
|
(not for the I/O operations)
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</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">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
A simple cache implementation that uses a java.util.ConcurrentHashMap as a storage backend
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="rbcs:cacheType">
|
||||||
|
<xs:attribute name="max-age" type="xs:duration" default="P1D">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Values will be removed from the cache after this amount of time
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="max-size" type="rbcs:byteSizeType" default="0x1000000">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The maximum allowed total size of the cache in bytes, old values will be purged from the cache
|
||||||
|
when the insertion of a new value causes this limit to be exceeded
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="digest" type="xs:token">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Hashing algorithm to apply to the key. If omitted, no hashing is performed.
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="enable-compression" type="xs:boolean" default="true">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Enable deflate compression for stored cache elements
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Deflate compression level to use for cache compression,
|
||||||
|
use -1 to use the default compression level of java.util.zip.Deflater
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="fileSystemCacheType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
A simple cache implementation that stores data in a folder on the filesystem
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="rbcs:cacheType">
|
||||||
|
<xs:attribute name="path" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
File system path that will be used to store the cache data files
|
||||||
|
(it will be created if it doesn't already exist)
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="max-age" type="xs:duration" default="P1D">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Values will be removed from the cache after this amount of time
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="digest" type="xs:token" default="SHA3-224">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Hashing algorithm to apply to the key. If omitted, no hashing is performed.
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="enable-compression" type="xs:boolean" default="true">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Enable deflate compression for stored cache elements
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Deflate compression level to use for cache compression,
|
||||||
|
use -1 to use the default compression level of java.util.zip.Deflater
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="tlsCertificateAuthorizationType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="group-extractor" type="rbcs:X500NameExtractorType" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
A regex based extractor that will be used to determine which group the client belongs to,
|
||||||
|
based on the X.500 name of the subject field in the client's TLS certificate.
|
||||||
|
When this is set RBAC works even if the user isn't listed in the <users/> section as
|
||||||
|
the client will be assigned role solely based on the group he is found to belong to.
|
||||||
|
Note that this does not allow for a client to be part of multiple groups.
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="user-extractor" type="rbcs:X500NameExtractorType" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
A regex based extractor that will be used to assign a user to a connected client,
|
||||||
|
based on the X.500 name of the subject field in the client's TLS certificate.
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="X500NameExtractorType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Extract informations from a client TLS certificates using
|
||||||
|
regular expressions applied to the X.500 name "Subject" field
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:attribute name="attribute-name" type="xs:token">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
X.500 name attribute to apply the regex
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="pattern" type="xs:token">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Regex that wil be applied to the attribute value,
|
||||||
|
use regex groups to extract relevant data
|
||||||
|
(note that only the first group that appears in the regex is used)
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="authorizationType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="users" type="rbcs:usersType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
List of users registered in the application
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="groups" type="rbcs:groupsType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
List of user groups registered in the application
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:unique name="groupKey">
|
||||||
|
<xs:selector xpath="group"/>
|
||||||
|
<xs:field xpath="@name"/>
|
||||||
|
</xs:unique>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="authenticationType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Authentication mechanism to assign usernames and groups to clients
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:choice>
|
||||||
|
<xs:element name="basic">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Enable HTTP basic authentication
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="client-certificate" type="rbcs:tlsCertificateAuthorizationType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Enable TLS certificate authentication
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="none">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Disable authentication altogether
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="quotaType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Defines a quota for a user or a group
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:attribute name="calls" type="xs:positiveInteger" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Maximum number of allowed calls in a given period
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="period" type="xs:duration" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The period length
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="max-available-calls" type="xs:positiveInteger" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Maximum number of available calls that can be accumulated
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="initial-available-calls" type="xs:unsignedInt" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Number of available calls for users at their first call
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="anonymousUserType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Placeholder for a client that is not authenticated
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Calls quota for the user
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="userType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
An authenticated user
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Calls quota for the user
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="name" type="xs:token" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
User's name
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="password" type="xs:string" use="optional">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
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>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="usersType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
List of registered users, add an <anonymous> tag to enable authenticated user access
|
||||||
|
when authentication is enabled
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="user" type="rbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xs:element name="anonymous" type="rbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="groupsType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
List of registered user groups
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="group" type="rbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="groupType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The definition of a user group, with the list of its member users
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="users" type="rbcs:userRefsType" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:unique name="userRefWriterKey">
|
||||||
|
<xs:selector xpath="user"/>
|
||||||
|
<xs:field xpath="@ref"/>
|
||||||
|
</xs:unique>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="roles" type="rbcs:rolesType" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The list of application roles awarded to all the members of this group
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="user-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The call quota for each user in this group
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="group-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The cumulative call quota for all users in this group
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="name" type="xs:token">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
The group's name
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<xs:complexType name="userRefsType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
A list of references to users in the <users> section
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="user" type="rbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/>
|
||||||
|
<xs:element name="anonymous" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="userRefType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
A reference to a user in the <users> section
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:attribute name="ref" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Name of the referenced user
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="tlsType">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Enable TLS protocol
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="keystore" type="rbcs:keyStoreType" >
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Path to the keystore file that contains the server's key and certificate
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="truststore" type="rbcs:trustStoreType" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Path to the truststore file that contains the trusted CAs
|
||||||
|
for TLS client certificate verification
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="keyStoreType">
|
||||||
|
<xs:attribute name="file" type="xs:string" use="required">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
System path to the keystore file
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="password" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Password to open they keystore file
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="key-alias" type="xs:string" 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">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Private key entry's encryption password
|
||||||
|
</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 trustore file
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="password" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
Trustore 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 certificate validity using CRL/OCSP
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="require-client-certificate" type="xs:boolean" use="optional" default="false">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>
|
||||||
|
If true, the server requires a TLS client certificate from the client and simply refuses to connect
|
||||||
|
when a client certificate isn't provided
|
||||||
|
</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="propertiesType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="rbcs:propertyType"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="propertyType">
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="xs:string">
|
||||||
|
<xs:attribute name="key" type="xs:string" use="required"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="hostAndPortType">
|
||||||
|
<xs:attribute name="host" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:simpleType name="byteSizeType">
|
||||||
|
<xs:restriction base="xs:token">
|
||||||
|
<xs:pattern value="(0x[a-f0-9]+|[0-9]+)"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
|
||||||
|
<xs:simpleType name="compressionLevelType">
|
||||||
|
<xs:restriction base="xs:integer">
|
||||||
|
<xs:minInclusive value="-1"/>
|
||||||
|
<xs:maxInclusive value="9"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
|
||||||
|
</xs:schema>
|
@@ -1,238 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<xs:schema targetNamespace="urn:net.woggioni.rbcs.server"
|
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
xmlns:rbcs="urn:net.woggioni.rbcs.server"
|
|
||||||
elementFormDefault="unqualified">
|
|
||||||
<xs:element name="server" type="rbcs:serverType"/>
|
|
||||||
|
|
||||||
<xs:complexType name="serverType">
|
|
||||||
<xs:sequence minOccurs="0">
|
|
||||||
<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="cache" type="rbcs:cacheType" maxOccurs="1"/>
|
|
||||||
<xs:element name="authorization" type="rbcs:authorizationType" minOccurs="0">
|
|
||||||
<xs:key name="userId">
|
|
||||||
<xs:selector xpath="users/user"/>
|
|
||||||
<xs:field xpath="@name"/>
|
|
||||||
</xs:key>
|
|
||||||
<xs:keyref name="userRef" refer="rbcs:userId">
|
|
||||||
<xs:selector xpath="groups/group/users/user"/>
|
|
||||||
<xs:field xpath="@ref"/>
|
|
||||||
</xs:keyref>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="authentication" type="rbcs:authenticationType" minOccurs="0" maxOccurs="1"/>
|
|
||||||
<xs:element name="tls" type="rbcs:tlsType" minOccurs="0" maxOccurs="1"/>
|
|
||||||
</xs:sequence>
|
|
||||||
<xs:attribute name="path" type="xs:string" use="optional"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="bindType">
|
|
||||||
<xs:attribute name="host" type="xs:token" use="required"/>
|
|
||||||
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
|
|
||||||
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024"/>
|
|
||||||
</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="max-request-size" type="rbcs:byteSizeType" use="optional" default="0x4000000"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="eventExecutorType">
|
|
||||||
<xs:attribute name="use-virtual-threads" type="xs:boolean" use="optional" default="true"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="cacheType" abstract="true"/>
|
|
||||||
|
|
||||||
<xs:complexType name="inMemoryCacheType">
|
|
||||||
<xs:complexContent>
|
|
||||||
<xs:extension base="rbcs:cacheType">
|
|
||||||
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
|
|
||||||
<xs:attribute name="max-size" type="rbcs:byteSizeType" default="0x1000000"/>
|
|
||||||
<xs:attribute name="digest" type="xs:token" default="MD5"/>
|
|
||||||
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
|
|
||||||
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/>
|
|
||||||
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
|
|
||||||
</xs:extension>
|
|
||||||
</xs:complexContent>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="fileSystemCacheType">
|
|
||||||
<xs:complexContent>
|
|
||||||
<xs:extension base="rbcs:cacheType">
|
|
||||||
<xs:attribute name="path" type="xs:string" use="optional"/>
|
|
||||||
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
|
|
||||||
<xs:attribute name="digest" type="xs:token" default="MD5"/>
|
|
||||||
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
|
|
||||||
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/>
|
|
||||||
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
|
|
||||||
</xs:extension>
|
|
||||||
</xs:complexContent>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="tlsCertificateAuthorizationType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="group-extractor" type="rbcs:X500NameExtractorType" minOccurs="0"/>
|
|
||||||
<xs:element name="user-extractor" type="rbcs:X500NameExtractorType" minOccurs="0"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="X500NameExtractorType">
|
|
||||||
<xs:attribute name="attribute-name" type="xs:token"/>
|
|
||||||
<xs:attribute name="pattern" type="xs:token"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="authorizationType">
|
|
||||||
<xs:all>
|
|
||||||
<xs:element name="users" type="rbcs:usersType"/>
|
|
||||||
<xs:element name="groups" type="rbcs:groupsType">
|
|
||||||
<xs:unique name="groupKey">
|
|
||||||
<xs:selector xpath="group"/>
|
|
||||||
<xs:field xpath="@name"/>
|
|
||||||
</xs:unique>
|
|
||||||
</xs:element>
|
|
||||||
</xs:all>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="authenticationType">
|
|
||||||
<xs:choice>
|
|
||||||
<xs:element name="basic"/>
|
|
||||||
<xs:element name="client-certificate" type="rbcs:tlsCertificateAuthorizationType"/>
|
|
||||||
<xs:element name="none"/>
|
|
||||||
</xs:choice>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="quotaType">
|
|
||||||
<xs:attribute name="calls" type="xs:positiveInteger" use="required"/>
|
|
||||||
<xs:attribute name="period" type="xs:duration" use="required"/>
|
|
||||||
<xs:attribute name="max-available-calls" type="xs:positiveInteger" use="optional"/>
|
|
||||||
<xs:attribute name="initial-available-calls" type="xs:unsignedInt" use="optional"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="anonymousUserType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="userType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
|
|
||||||
</xs:sequence>
|
|
||||||
<xs:attribute name="name" type="xs:token" use="required"/>
|
|
||||||
<xs:attribute name="password" type="xs:string" use="optional"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="usersType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="user" type="rbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
<xs:element name="anonymous" type="rbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="groupsType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="group" type="rbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="groupType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="users" type="rbcs:userRefsType" maxOccurs="1" minOccurs="0">
|
|
||||||
<xs:unique name="userRefWriterKey">
|
|
||||||
<xs:selector xpath="user"/>
|
|
||||||
<xs:field xpath="@ref"/>
|
|
||||||
</xs:unique>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="roles" type="rbcs:rolesType" maxOccurs="1" minOccurs="0"/>
|
|
||||||
<xs:element name="user-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
|
|
||||||
<xs:element name="group-quota" type="rbcs:quotaType" minOccurs="0" maxOccurs="1"/>
|
|
||||||
</xs:sequence>
|
|
||||||
<xs:attribute name="name" type="xs:token"/>
|
|
||||||
</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:choice>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="userRefsType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="user" type="rbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/>
|
|
||||||
<xs:element name="anonymous" minOccurs="0" maxOccurs="1"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="userRefType">
|
|
||||||
<xs:attribute name="ref" type="xs:string" use="required"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="tlsType">
|
|
||||||
<xs:all>
|
|
||||||
<xs:element name="keystore" type="rbcs:keyStoreType" />
|
|
||||||
<xs:element name="truststore" type="rbcs:trustStoreType" minOccurs="0"/>
|
|
||||||
</xs:all>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="keyStoreType">
|
|
||||||
<xs:attribute name="file" type="xs:string" use="required"/>
|
|
||||||
<xs:attribute name="password" type="xs:string"/>
|
|
||||||
<xs:attribute name="key-alias" type="xs:string" use="required"/>
|
|
||||||
<xs:attribute name="key-password" type="xs:string"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="trustStoreType">
|
|
||||||
<xs:attribute name="file" type="xs:string" use="required"/>
|
|
||||||
<xs:attribute name="password" type="xs:string"/>
|
|
||||||
<xs:attribute name="check-certificate-status" type="xs:boolean"/>
|
|
||||||
<xs:attribute name="require-client-certificate" type="xs:boolean" use="optional" default="false"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="propertiesType">
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="rbcs:propertyType"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="propertyType">
|
|
||||||
<xs:simpleContent>
|
|
||||||
<xs:extension base="xs:string">
|
|
||||||
<xs:attribute name="key" type="xs:string" use="required"/>
|
|
||||||
</xs:extension>
|
|
||||||
</xs:simpleContent>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:complexType name="hostAndPortType">
|
|
||||||
<xs:attribute name="host" type="xs:string" use="required"/>
|
|
||||||
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
|
|
||||||
</xs:complexType>
|
|
||||||
|
|
||||||
<xs:simpleType name="byteSizeType">
|
|
||||||
<xs:restriction base="xs:token">
|
|
||||||
<xs:pattern value="(0x[a-f0-9]+|[0-9]+)"/>
|
|
||||||
</xs:restriction>
|
|
||||||
</xs:simpleType>
|
|
||||||
|
|
||||||
<xs:simpleType name="compressionLevelType">
|
|
||||||
<xs:restriction base="xs:integer">
|
|
||||||
<xs:minInclusive value="-1"/>
|
|
||||||
<xs:maxInclusive value="9"/>
|
|
||||||
</xs:restriction>
|
|
||||||
</xs:simpleType>
|
|
||||||
|
|
||||||
</xs:schema>
|
|
@@ -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
|
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.URI
|
||||||
import java.net.http.HttpRequest
|
import java.net.http.HttpRequest
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@@ -15,6 +9,12 @@ import java.time.temporal.ChronoUnit
|
|||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.zip.Deflater
|
import java.util.zip.Deflater
|
||||||
import kotlin.random.Random
|
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() {
|
abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
||||||
@@ -33,17 +33,17 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
|||||||
this.cacheDir = testDir.resolve("cache")
|
this.cacheDir = testDir.resolve("cache")
|
||||||
cfg = Configuration.of(
|
cfg = Configuration.of(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
NetworkUtils.getFreePort(),
|
getFreePort(),
|
||||||
50,
|
50,
|
||||||
serverPath,
|
serverPath,
|
||||||
Configuration.EventExecutor(false),
|
Configuration.EventExecutor(false),
|
||||||
|
Configuration.RateLimiter(true, 0x100000, 50),
|
||||||
Configuration.Connection(
|
Configuration.Connection(
|
||||||
Duration.of(10, ChronoUnit.SECONDS),
|
|
||||||
Duration.of(10, ChronoUnit.SECONDS),
|
|
||||||
Duration.of(60, ChronoUnit.SECONDS),
|
Duration.of(60, ChronoUnit.SECONDS),
|
||||||
Duration.of(30, ChronoUnit.SECONDS),
|
Duration.of(30, ChronoUnit.SECONDS),
|
||||||
Duration.of(30, ChronoUnit.SECONDS),
|
Duration.of(30, ChronoUnit.SECONDS),
|
||||||
0x1000
|
0x1000,
|
||||||
|
0x10000
|
||||||
),
|
),
|
||||||
users.asSequence().map { it.name to it}.toMap(),
|
users.asSequence().map { it.name to it}.toMap(),
|
||||||
sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
|
sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
|
||||||
@@ -52,8 +52,7 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
|
|||||||
maxAge = Duration.ofSeconds(3600 * 24),
|
maxAge = Duration.ofSeconds(3600 * 24),
|
||||||
digestAlgorithm = "MD5",
|
digestAlgorithm = "MD5",
|
||||||
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
||||||
compressionEnabled = false,
|
compressionEnabled = false
|
||||||
chunkSize = 0x1000
|
|
||||||
),
|
),
|
||||||
Configuration.BasicAuthentication(),
|
Configuration.BasicAuthentication(),
|
||||||
null,
|
null,
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package net.woggioni.rbcs.server.test
|
package net.woggioni.rbcs.server.test
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
import net.woggioni.rbcs.api.Configuration
|
import net.woggioni.rbcs.api.Configuration
|
||||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer
|
import net.woggioni.rbcs.server.RemoteBuildCacheServer
|
||||||
import org.junit.jupiter.api.AfterAll
|
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.TestInstance
|
||||||
import org.junit.jupiter.api.TestMethodOrder
|
import org.junit.jupiter.api.TestMethodOrder
|
||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user