Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
17215b401a
|
|||
4aced1c717
|
|||
31ce34cddb
|
|||
d64f7f4f27
|
|||
d15235fc4c
|
|||
49bb4f41b8
|
|||
a1398045ac
|
|||
1f93602102
|
|||
c818463a2e
|
|||
cd28563985
|
|||
8ef2d9c64e
|
|||
1510956989
|
|||
ac4f0fdd19
|
|||
37da03c719
|
|||
60bc4375cf
|
|||
725fe22b80
|
|||
ca18b63f27
|
|||
23f2a351a6
|
|||
c7d2b89d82
|
|||
72c34b57a6
|
|||
619873c4a9
|
|||
591f6e2af4
|
|||
ad00ebee9b
|
|||
adf8a0cf24
|
|||
42eb26a948
|
|||
f048a60540
|
|||
0463038aaa
|
|||
7eca8a270d
|
|||
84d7c977f9
|
|||
317eadce07
|
|||
af79e74b95
|
|||
78ae21caa4
|
|||
6c0eadb9fb
|
|||
5fef1b932e
|
|||
5e173dbf62
|
|||
53b24e3d54
|
|||
7d0f24fa58
|
|||
1b6cf1bd96
|
|||
4180df2352
|
|||
c2e388b931
|
|||
6c62ac85c0
|
|||
89153b60f8
|
|||
a2a40ab60f
|
|||
45458761f3
|
|||
90a5834f5f
|
|||
1823d0b9ca
|
|||
649cbba954
|
|||
eb9ccce3be
|
|||
316f64cf9d
|
|||
24a49779f9
|
|||
423b749db9
|
|||
9ce3e7fa0a
|
|||
1e6ece37a5
|
|||
fc9900d821
|
|||
1a78c8092b
|
|||
3d1847c408
|
|||
702556bfbb
|
|||
06e9e7ca09
|
|||
fa5bb55baa
|
|||
007d0fffd6
|
|||
75ebf2248f
|
|||
241d95fe1c
|
|||
3b7030c302
|
|||
a8670277e7
|
|||
03ee75266d
|
@@ -9,11 +9,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: graalvm
|
||||
java-version: 21
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
- name: Execute Gradle build
|
||||
@@ -36,7 +31,7 @@ jobs:
|
||||
username: woggioni
|
||||
password: ${{ secrets.PUBLISHER_TOKEN }}
|
||||
-
|
||||
name: Build gbcs Docker image
|
||||
name: Build rbcs Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: "docker/build/docker"
|
||||
@@ -44,12 +39,12 @@ jobs:
|
||||
push: true
|
||||
pull: true
|
||||
tags: |
|
||||
gitea.woggioni.net/woggioni/gbcs:latest
|
||||
gitea.woggioni.net/woggioni/gbcs:${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
|
||||
gitea.woggioni.net/woggioni/rbcs:vanilla
|
||||
gitea.woggioni.net/woggioni/rbcs:vanilla-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release-vanilla
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
-
|
||||
name: Build gbcs memcached Docker image
|
||||
name: Build rbcs memcache Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: "docker/build/docker"
|
||||
@@ -57,11 +52,25 @@ jobs:
|
||||
push: true
|
||||
pull: true
|
||||
tags: |
|
||||
gitea.woggioni.net/woggioni/gbcs:memcached
|
||||
gitea.woggioni.net/woggioni/gbcs:memcached-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release-memcached
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
|
||||
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/gbcs:buildx
|
||||
gitea.woggioni.net/woggioni/rbcs:memcache
|
||||
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release-memcache
|
||||
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/rbcs:buildx
|
||||
-
|
||||
name: Build rbcs native Docker image
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: "docker/build/docker"
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
pull: true
|
||||
tags: |
|
||||
gitea.woggioni.net/woggioni/rbcs:latest
|
||||
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
|
||||
gitea.woggioni.net/woggioni/rbcs:native
|
||||
gitea.woggioni.net/woggioni/rbcs:native-${{ steps.retrieve-version.outputs.VERSION }}
|
||||
target: release-native
|
||||
- name: Publish artifacts
|
||||
env:
|
||||
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,4 +4,4 @@
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
gbcs-cli/native-image/*.json
|
||||
rbcs-cli/native-image/*.json
|
||||
|
@@ -1,2 +0,0 @@
|
||||
FROM gitea.woggioni.net/woggioni/gbcs:memcached
|
||||
COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml
|
20
LICENSE
Normal file
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.
|
429
README.md
Normal file
429
README.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
|
56
build.gradle
56
build.gradle
@@ -1,9 +1,7 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
alias catalog.plugins.kotlin.jvm
|
||||
alias catalog.plugins.kotlin.jvm apply false
|
||||
alias catalog.plugins.sambal
|
||||
alias catalog.plugins.lombok
|
||||
id 'maven-publish'
|
||||
alias catalog.plugins.lombok apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +14,7 @@ allprojects { subproject ->
|
||||
if(project.currentTag.isPresent()) {
|
||||
version = project.currentTag.map { it[0] }.get()
|
||||
} else {
|
||||
version = project.gitRevision.map { gitRevision ->
|
||||
"${getProperty('gbcs.version')}.${gitRevision[0..10]}"
|
||||
}.get()
|
||||
version = "${getProperty('rbcs.version')}-SNAPSHOT"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -26,7 +22,6 @@ allprojects { subproject ->
|
||||
url = getProperty('gitea.maven.url')
|
||||
content {
|
||||
includeModule 'net.woggioni', 'jwo'
|
||||
includeModule 'net.woggioni', 'xmemcached'
|
||||
includeGroup 'com.lys'
|
||||
}
|
||||
}
|
||||
@@ -44,10 +39,16 @@ allprojects { subproject ->
|
||||
modularity.inferModulePath = true
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
vendor = JvmVendorSpec.ORACLE
|
||||
vendor = JvmVendorSpec.GRAAL_VM
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation catalog.junit.jupiter.api
|
||||
testImplementation catalog.junit.jupiter.params
|
||||
testRuntimeOnly catalog.junit.jupiter.engine
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -68,6 +69,15 @@ allprojects { subproject ->
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin('jacoco') {
|
||||
test {
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) {
|
||||
tasks.withType(KotlinCompile.class) {
|
||||
compilerOptions.jvmTarget = JvmTarget.JVM_21
|
||||
@@ -102,34 +112,6 @@ allprojects { subproject ->
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation catalog.jwo
|
||||
implementation catalog.slf4j.api
|
||||
implementation catalog.netty.codec.http
|
||||
|
||||
api project('gbcs-base')
|
||||
api project('gbcs-api')
|
||||
|
||||
// runtimeOnly catalog.slf4j.jdk14
|
||||
testRuntimeOnly catalog.logback.classic
|
||||
|
||||
testImplementation catalog.bcprov.jdk18on
|
||||
testImplementation catalog.bcpkix.jdk18on
|
||||
testImplementation catalog.junit.jupiter.api
|
||||
testImplementation catalog.junit.jupiter.params
|
||||
testRuntimeOnly catalog.junit.jupiter.engine
|
||||
|
||||
testRuntimeOnly project("gbcs-memcached")
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('version') {
|
||||
doLast {
|
||||
println("VERSION=$version")
|
||||
|
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<gbcs:server useVirtualThreads="true" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:gbcs="urn:net.woggioni.gbcs"
|
||||
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached"
|
||||
xs:schemaLocation="urn:net.woggioni.gbcs-memcached jpms://net.woggioni.gbcs.memcached/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd">
|
||||
<bind host="0.0.0.0" port="13080" />
|
||||
<cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="16777216" compression-mode="zip">
|
||||
<server host="memcached" port="11211"/>
|
||||
</cache>
|
||||
<authentication>
|
||||
<none/>
|
||||
</authentication>
|
||||
</gbcs:server>
|
@@ -15,7 +15,4 @@
|
||||
<root level="info">
|
||||
<appender-ref ref="console"/>
|
||||
</root>
|
||||
<logger name="io.netty" level="debug"/>
|
||||
<logger name="com.google.code.yanf4j" level="warn"/>
|
||||
<logger name="net.rubyeye.xmemcached" level="warn"/>
|
||||
</configuration>
|
87
doc/benchmarks.md
Normal file
87
doc/benchmarks.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# RBCS performance benchmarks
|
||||
|
||||
All test were executed under the following conditions:
|
||||
- CPU: Intel Celeron J3455 (4 physical cores)
|
||||
- memory: 8GB DDR3L 1600 MHz
|
||||
- disk: SATA3 120GB SSD
|
||||
- HTTP compression: disabled
|
||||
- cache compression: disabled
|
||||
- digest: none
|
||||
- authentication: disabled
|
||||
- TLS: disabled
|
||||
- network RTT: 14ms
|
||||
- network bandwidth: 112 MiB/s
|
||||
### In memory cache backend
|
||||
|
||||
|
||||
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|
||||
|----------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 10 | 3691 | 4037 |
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 100 | 6881 | 7483 |
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 10 | 3790 | 4069 |
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 100 | 6716 | 7408 |
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 10 | 3399 | 1974 |
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 100 | 5341 | 6402 |
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 10 | 1099 | 1116 |
|
||||
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 100 | 1379 | 1703 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 10 | 4443 | 5170 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 100 | 12813 | 13568 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 10 | 4450 | 4383 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 100 | 12212 | 13586 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 10 | 3441 | 3012 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 100 | 8982 | 10452 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 10 | 1391 | 1167 |
|
||||
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 100 | 1303 | 1151 |
|
||||
|
||||
### Filesystem cache backend
|
||||
|
||||
compression: disabled
|
||||
digest: none
|
||||
authentication: disabled
|
||||
TLS: disabled
|
||||
|
||||
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|
||||
|---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 10 | 1208 | 2048 |
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 1304 | 2394 |
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 1408 | 2157 |
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 1282 | 1888 |
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 1291 | 1256 |
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 1170 | 1423 |
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 313 | 606 |
|
||||
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 298 | 609 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 2195 | 3477 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 2480 | 6207 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 2164 | 3413 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 2842 | 6218 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 1302 | 2591 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 2270 | 3045 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 375 | 394 |
|
||||
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 364 | 462 |
|
||||
|
||||
|
||||
### Memcache cache backend
|
||||
|
||||
compression: disabled
|
||||
digest: MD5
|
||||
authentication: disabled
|
||||
TLS: disabled
|
||||
|
||||
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|
||||
|---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 10 | 2505 | 2578 |
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 3582 | 3935 |
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 2495 | 2784 |
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 3565 | 3883 |
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 2174 | 2505 |
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 2937 | 3563 |
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 648 | 1074 |
|
||||
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 724 | 1548 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 2362 | 2927 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 5491 | 6531 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 2125 | 2807 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 5173 | 6242 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 1720 | 2397 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 3871 | 5859 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 616 | 1016 |
|
||||
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 820 | 1677 |
|
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.
|
191
doc/server_configuration.md
Normal file
191
doc/server_configuration.md
Normal file
@@ -0,0 +1,191 @@
|
||||
### 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
|
||||
|
||||
#### `<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)
|
||||
- `chunk-size` (default: 0x10000): Maximum socket write size
|
||||
|
||||
##### 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
|
||||
- `chunk-size` (default: 0x10000): Maximum in-memory cache value size
|
||||
|
||||
#### `<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"
|
||||
read-timeout="PT5S"
|
||||
write-timeout="PT5S"/>
|
||||
<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" chunk-size="0x1000" 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>
|
||||
|
||||
```
|
@@ -1,36 +0,0 @@
|
||||
networks:
|
||||
default:
|
||||
external: false
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.118.0.0/16
|
||||
ip_range: 172.118.0.0/16
|
||||
gateway: 172.118.0.254
|
||||
services:
|
||||
gbcs:
|
||||
build:
|
||||
context: .
|
||||
container_name: gbcs
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8080:13080"
|
||||
- "[::1]:8080:13080"
|
||||
depends_on:
|
||||
memcached:
|
||||
condition: service_started
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "2.00"
|
||||
memory: 256M
|
||||
memcached:
|
||||
image: memcached
|
||||
container_name: memcached
|
||||
restart: unless-stopped
|
||||
command: -I 64m -m 900m
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.00"
|
||||
memory: 1G
|
@@ -1,21 +1,23 @@
|
||||
FROM alpine:latest AS base-release
|
||||
RUN --mount=type=cache,target=/var/cache/apk apk update
|
||||
RUN --mount=type=cache,target=/var/cache/apk apk add openjdk21-jre
|
||||
FROM eclipse-temurin:21-jre-alpine AS base-release
|
||||
RUN adduser -D luser
|
||||
USER luser
|
||||
WORKDIR /home/luser
|
||||
|
||||
FROM base-release AS release
|
||||
ADD gbcs-cli-envelope-*.jar gbcs.jar
|
||||
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"]
|
||||
FROM base-release AS release-vanilla
|
||||
ADD rbcs-cli-envelope-*.jar rbcs.jar
|
||||
ENTRYPOINT ["java", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"]
|
||||
|
||||
FROM base-release AS release-memcached
|
||||
ADD --chown=luser:luser gbcs-cli-envelope-*.jar gbcs.jar
|
||||
FROM base-release AS release-memcache
|
||||
ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.jar
|
||||
RUN mkdir plugins
|
||||
WORKDIR /home/luser/plugins
|
||||
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/gbcs-memcached*.tar
|
||||
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar
|
||||
WORKDIR /home/luser
|
||||
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"]
|
||||
ADD logback.xml .
|
||||
ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"]
|
||||
|
||||
FROM release-memcached as compose
|
||||
COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml
|
||||
FROM scratch AS release-native
|
||||
ADD rbcs-cli.upx /rbcs/rbcs-cli
|
||||
ENV RBCS_CONFIGURATION_DIR="/rbcs"
|
||||
WORKDIR /rbcs
|
||||
ENTRYPOINT ["/rbcs/rbcs-cli"]
|
||||
|
24
docker/README.md
Normal file
24
docker/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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
|
||||
that has no userspace dependencies. It also embeds the memcache plugin inside the executable.
|
||||
Use this image for maximum efficiency and minimal memory footprint.
|
||||
|
||||
## Which image shoud 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.
|
@@ -18,8 +18,8 @@ configurations {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
docker project(path: ':gbcs-cli', configuration: 'release')
|
||||
docker project(path: ':gbcs-memcached', configuration: 'release')
|
||||
docker project(path: ':rbcs-cli', configuration: 'release')
|
||||
docker project(path: ':rbcs-server-memcache', configuration: 'release')
|
||||
}
|
||||
|
||||
Provider<Task> cleanTaskProvider = tasks.named(BasePlugin.CLEAN_TASK_NAME) {}
|
||||
@@ -30,38 +30,39 @@ Provider<Copy> prepareDockerBuild = tasks.register('prepareDockerBuild', Copy) {
|
||||
into project.layout.buildDirectory.file('docker')
|
||||
from(configurations.docker)
|
||||
from(file('Dockerfile'))
|
||||
from(rootProject.file('conf')) {
|
||||
include 'logback.xml'
|
||||
}
|
||||
}
|
||||
|
||||
Provider<DockerBuildImage> dockerBuild = tasks.register('dockerBuildImage', DockerBuildImage) {
|
||||
group = 'docker'
|
||||
dependsOn prepareDockerBuild
|
||||
images.add('gitea.woggioni.net/woggioni/gbcs:latest')
|
||||
images.add("gitea.woggioni.net/woggioni/gbcs:${version}")
|
||||
images.add('gitea.woggioni.net/woggioni/rbcs:latest')
|
||||
images.add("gitea.woggioni.net/woggioni/rbcs:${version}")
|
||||
}
|
||||
|
||||
Provider<DockerTagImage> dockerTag = tasks.register('dockerTagImage', DockerTagImage) {
|
||||
group = 'docker'
|
||||
repository = 'gitea.woggioni.net/woggioni/gbcs'
|
||||
imageId = 'gitea.woggioni.net/woggioni/gbcs:latest'
|
||||
repository = 'gitea.woggioni.net/woggioni/rbcs'
|
||||
imageId = 'gitea.woggioni.net/woggioni/rbcs:latest'
|
||||
tag = version
|
||||
}
|
||||
|
||||
Provider<DockerTagImage> dockerTagMemcached = tasks.register('dockerTagMemcachedImage', DockerTagImage) {
|
||||
Provider<DockerTagImage> dockerTagMemcache = tasks.register('dockerTagMemcacheImage', DockerTagImage) {
|
||||
group = 'docker'
|
||||
repository = 'gitea.woggioni.net/woggioni/gbcs'
|
||||
imageId = 'gitea.woggioni.net/woggioni/gbcs:memcached'
|
||||
tag = "${version}-memcached"
|
||||
repository = 'gitea.woggioni.net/woggioni/rbcs'
|
||||
imageId = 'gitea.woggioni.net/woggioni/rbcs:memcache'
|
||||
tag = "${version}-memcache"
|
||||
}
|
||||
|
||||
Provider<DockerPushImage> dockerPush = tasks.register('dockerPushImage', DockerPushImage) {
|
||||
group = 'docker'
|
||||
dependsOn dockerTag, dockerTagMemcached
|
||||
dependsOn dockerTag, dockerTagMemcache
|
||||
registryCredentials {
|
||||
url = getProperty('docker.registry.url')
|
||||
username = 'woggioni'
|
||||
password = System.getenv().get("PUBLISHER_TOKEN")
|
||||
}
|
||||
images = [dockerTag.flatMap{ it.tag }, dockerTagMemcached.flatMap{ it.tag }]
|
||||
images = [dockerTag.flatMap{ it.tag }, dockerTagMemcache.flatMap{ it.tag }]
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,6 +0,0 @@
|
||||
module net.woggioni.gbcs.api {
|
||||
requires static lombok;
|
||||
requires java.xml;
|
||||
exports net.woggioni.gbcs.api;
|
||||
exports net.woggioni.gbcs.api.exception;
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
|
||||
import net.woggioni.gbcs.api.exception.ContentTooLargeException;
|
||||
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
public interface Cache extends AutoCloseable {
|
||||
ReadableByteChannel get(String key);
|
||||
|
||||
void put(String key, byte[] content) throws ContentTooLargeException;
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
|
||||
public enum Role {
|
||||
Reader, Writer
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package net.woggioni.gbcs.api.exception;
|
||||
|
||||
public class GbcsException extends RuntimeException {
|
||||
public GbcsException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
module net.woggioni.gbcs.base {
|
||||
requires java.xml;
|
||||
requires java.logging;
|
||||
requires org.slf4j;
|
||||
requires kotlin.stdlib;
|
||||
|
||||
exports net.woggioni.gbcs.base;
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
package net.woggioni.gbcs.base
|
||||
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
|
||||
object GBCS {
|
||||
fun String.toUrl() : URL = URL.of(URI(this), null)
|
||||
|
||||
const val GBCS_NAMESPACE_URI: String = "urn:net.woggioni.gbcs"
|
||||
const val GBCS_PREFIX: String = "gbcs"
|
||||
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
}
|
@@ -1,104 +0,0 @@
|
||||
package net.woggioni.gbcs.base
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.logging.LogManager
|
||||
|
||||
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
|
||||
|
||||
inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) {
|
||||
if(isTraceEnabled) {
|
||||
val (format, params) = messageBuilder()
|
||||
trace(format, params)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.debugParam(messageBuilder : () -> Pair<String, Array<Any>>) {
|
||||
if(isDebugEnabled) {
|
||||
val (format, params) = messageBuilder()
|
||||
info(format, params)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.infoParam(messageBuilder : () -> Pair<String, Array<Any>>) {
|
||||
if(isInfoEnabled) {
|
||||
val (format, params) = messageBuilder()
|
||||
info(format, params)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.warnParam(messageBuilder : () -> Pair<String, Array<Any>>) {
|
||||
if(isWarnEnabled) {
|
||||
val (format, params) = messageBuilder()
|
||||
warn(format, params)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.errorParam(messageBuilder : () -> Pair<String, Array<Any>>) {
|
||||
if(isErrorEnabled) {
|
||||
val (format, params) = messageBuilder()
|
||||
error(format, params)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inline fun log(log : Logger,
|
||||
filter : Logger.() -> Boolean,
|
||||
loggerMethod : Logger.(String) -> Unit, messageBuilder : () -> String) {
|
||||
if(log.filter()) {
|
||||
log.loggerMethod(messageBuilder())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.trace(messageBuilder : () -> String) {
|
||||
if(isTraceEnabled) {
|
||||
trace(messageBuilder())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.debug(messageBuilder : () -> String) {
|
||||
if(isDebugEnabled) {
|
||||
debug(messageBuilder())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.info(messageBuilder : () -> String) {
|
||||
if(isInfoEnabled) {
|
||||
info(messageBuilder())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.warn(messageBuilder : () -> String) {
|
||||
if(isWarnEnabled) {
|
||||
warn(messageBuilder())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Logger.error(messageBuilder : () -> String) {
|
||||
if(isErrorEnabled) {
|
||||
error(messageBuilder())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LoggingConfig {
|
||||
|
||||
init {
|
||||
val logManager = LogManager.getLogManager()
|
||||
System.getProperty("log.config.source")?.let withSource@ { source ->
|
||||
val urls = LoggingConfig::class.java.classLoader.getResources(source)
|
||||
while(urls.hasMoreElements()) {
|
||||
val url = urls.nextElement()
|
||||
url.openStream().use { inputStream ->
|
||||
logManager.readConfiguration(inputStream)
|
||||
return@withSource
|
||||
}
|
||||
}
|
||||
Path.of(source).takeIf(Files::exists)
|
||||
?.let(Files::newInputStream)
|
||||
?.use(logManager::readConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
package net.woggioni.gbcs.base
|
||||
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.KeySpec
|
||||
import java.util.Base64
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
|
||||
object PasswordSecurity {
|
||||
private const val KEY_LENGTH = 256
|
||||
|
||||
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
|
||||
val result = ByteArray(arr1.size + arr2.size)
|
||||
var j = 0
|
||||
for(element in arr1) {
|
||||
result[j] = element
|
||||
j += 1
|
||||
}
|
||||
for(element in arr2) {
|
||||
result[j] = element
|
||||
j += 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun hashPassword(password : String, salt : String? = null) : String {
|
||||
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
|
||||
val result = ByteArray(16)
|
||||
nextBytes(result)
|
||||
result
|
||||
}
|
||||
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, 10, KEY_LENGTH)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val hash = factory.generateSecret(spec).encoded
|
||||
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
|
||||
}
|
||||
|
||||
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> {
|
||||
val decoded = Base64.getDecoder().decode(passwordHash)
|
||||
val hash = ByteArray(KEY_LENGTH / 8)
|
||||
val salt = ByteArray(decoded.size - KEY_LENGTH / 8)
|
||||
System.arraycopy(decoded, 0, hash, 0, hash.size)
|
||||
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
|
||||
return hash to salt
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
alias catalog.plugins.kotlin.jvm
|
||||
alias catalog.plugins.envelope
|
||||
alias catalog.plugins.sambal
|
||||
alias catalog.plugins.graalvm.native.image
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
|
||||
import net.woggioni.gradle.envelope.EnvelopeJarTask
|
||||
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
|
||||
import net.woggioni.gradle.graalvm.NativeImagePlugin
|
||||
import net.woggioni.gradle.graalvm.NativeImageTask
|
||||
|
||||
Property<String> mainClassName = objects.property(String.class)
|
||||
mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
|
||||
|
||||
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
|
||||
options.javaModuleMainClass = mainClassName
|
||||
}
|
||||
|
||||
configurations {
|
||||
release {
|
||||
transitive = false
|
||||
canBeConsumed = true
|
||||
canBeResolved = true
|
||||
visible = true
|
||||
}
|
||||
}
|
||||
|
||||
envelopeJar {
|
||||
mainModule = 'net.woggioni.gbcs.cli'
|
||||
mainClass = mainClassName
|
||||
|
||||
extraClasspath = ["plugins"]
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation catalog.jwo
|
||||
implementation catalog.slf4j.api
|
||||
implementation catalog.netty.codec.http
|
||||
implementation catalog.picocli
|
||||
|
||||
implementation project(":gbcs-client")
|
||||
implementation rootProject
|
||||
|
||||
// runtimeOnly catalog.slf4j.jdk14
|
||||
runtimeOnly catalog.logback.classic
|
||||
}
|
||||
|
||||
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
|
||||
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
|
||||
// systemProperties['log.config.source'] = 'logging.properties'
|
||||
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/cli/logback.xml'
|
||||
}
|
||||
|
||||
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
|
||||
mainClass = 'net.woggioni.gbcs.GraalNativeImageConfiguration'
|
||||
}
|
||||
|
||||
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
|
||||
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer'
|
||||
useMusl = true
|
||||
buildStaticImage = true
|
||||
}
|
||||
|
||||
artifacts {
|
||||
release(envelopeJarTaskProvider)
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
artifact envelopeJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,2 +0,0 @@
|
||||
Args=-H:Optimize=3 --gc=serial
|
||||
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils
|
@@ -1,16 +0,0 @@
|
||||
module net.woggioni.gbcs.cli {
|
||||
requires org.slf4j;
|
||||
requires net.woggioni.gbcs;
|
||||
requires info.picocli;
|
||||
requires net.woggioni.gbcs.base;
|
||||
requires net.woggioni.gbcs.client;
|
||||
requires kotlin.stdlib;
|
||||
requires net.woggioni.jwo;
|
||||
|
||||
exports net.woggioni.gbcs.cli.impl.converters to info.picocli;
|
||||
opens net.woggioni.gbcs.cli.impl.commands to info.picocli;
|
||||
opens net.woggioni.gbcs.cli.impl to info.picocli;
|
||||
opens net.woggioni.gbcs.cli to info.picocli, net.woggioni.gbcs.base;
|
||||
|
||||
exports net.woggioni.gbcs.cli;
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
package net.woggioni.gbcs.cli
|
||||
|
||||
import net.woggioni.gbcs.base.GbcsUrlStreamHandlerFactory
|
||||
import net.woggioni.gbcs.base.contextLogger
|
||||
import net.woggioni.gbcs.cli.impl.AbstractVersionProvider
|
||||
import net.woggioni.gbcs.cli.impl.GbcsCommand
|
||||
import net.woggioni.gbcs.cli.impl.commands.BenchmarkCommand
|
||||
import net.woggioni.gbcs.cli.impl.commands.ClientCommand
|
||||
import net.woggioni.gbcs.cli.impl.commands.PasswordHashCommand
|
||||
import net.woggioni.gbcs.cli.impl.commands.ServerCommand
|
||||
import net.woggioni.jwo.Application
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Model.CommandSpec
|
||||
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "gbcs", versionProvider = GradleBuildCacheServerCli.VersionProvider::class
|
||||
)
|
||||
class GradleBuildCacheServerCli : GbcsCommand() {
|
||||
|
||||
class VersionProvider : AbstractVersionProvider()
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(vararg args: String) {
|
||||
Thread.currentThread().contextClassLoader = GradleBuildCacheServerCli::class.java.classLoader
|
||||
GbcsUrlStreamHandlerFactory.install()
|
||||
val log = contextLogger()
|
||||
val app = Application.builder("gbcs")
|
||||
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
|
||||
.configurationDirectoryPropertyKey("net.woggioni.gbcs.conf.dir")
|
||||
.build()
|
||||
val gbcsCli = GradleBuildCacheServerCli()
|
||||
val commandLine = CommandLine(gbcsCli)
|
||||
commandLine.setExecutionExceptionHandler { ex, cl, parseResult ->
|
||||
log.error(ex.message, ex)
|
||||
CommandLine.ExitCode.SOFTWARE
|
||||
}
|
||||
commandLine.addSubcommand(ServerCommand(app))
|
||||
commandLine.addSubcommand(PasswordHashCommand())
|
||||
commandLine.addSubcommand(
|
||||
CommandLine(ClientCommand(app)).apply {
|
||||
addSubcommand(BenchmarkCommand())
|
||||
})
|
||||
System.exit(commandLine.execute(*args))
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Option(names = ["-V", "--version"], versionHelp = true)
|
||||
var versionHelp = false
|
||||
private set
|
||||
|
||||
@CommandLine.Spec
|
||||
private lateinit var spec: CommandSpec
|
||||
|
||||
|
||||
override fun run() {
|
||||
spec.commandLine().usage(System.out);
|
||||
}
|
||||
}
|
@@ -1,119 +0,0 @@
|
||||
package net.woggioni.gbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.gbcs.base.contextLogger
|
||||
import net.woggioni.gbcs.base.error
|
||||
import net.woggioni.gbcs.base.info
|
||||
import net.woggioni.gbcs.cli.impl.GbcsCommand
|
||||
import net.woggioni.gbcs.client.GbcsClient
|
||||
import picocli.CommandLine
|
||||
import java.security.SecureRandom
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.Base64
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import kotlin.random.Random
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "benchmark",
|
||||
description = ["Run a load test against the server"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class BenchmarkCommand : GbcsCommand() {
|
||||
private val log = contextLogger()
|
||||
|
||||
@CommandLine.Spec
|
||||
private lateinit var spec: CommandLine.Model.CommandSpec
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-e", "--entries"],
|
||||
description = ["Total number of elements to be added to the cache"],
|
||||
paramLabel = "NUMBER_OF_ENTRIES"
|
||||
)
|
||||
private var numberOfEntries = 1000
|
||||
|
||||
override fun run() {
|
||||
val clientCommand = spec.parent().userObject() as ClientCommand
|
||||
val profile = clientCommand.profileName.let { profileName ->
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
val client = GbcsClient(profile)
|
||||
|
||||
val entryGenerator = sequence {
|
||||
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
|
||||
while (true) {
|
||||
val key = Base64.getUrlEncoder().encode(random.nextBytes(16)).toString(Charsets.UTF_8)
|
||||
val value = random.nextBytes(0x1000)
|
||||
yield(key to value)
|
||||
}
|
||||
}
|
||||
|
||||
val entries = let {
|
||||
val completionQueue = LinkedBlockingQueue<Future<Pair<String, ByteArray>>>(numberOfEntries)
|
||||
val start = Instant.now()
|
||||
entryGenerator.take(numberOfEntries).forEach { entry ->
|
||||
val future = client.put(entry.first, entry.second).thenApply { entry }
|
||||
future.whenComplete { _, _ ->
|
||||
completionQueue.put(future)
|
||||
}
|
||||
}
|
||||
|
||||
val inserted = sequence<Pair<String, ByteArray>> {
|
||||
var completionCounter = 0
|
||||
while (completionCounter < numberOfEntries) {
|
||||
val future = completionQueue.take()
|
||||
try {
|
||||
yield(future.get())
|
||||
} catch (ee: ExecutionException) {
|
||||
val cause = ee.cause ?: ee
|
||||
log.error(cause.message, cause)
|
||||
}
|
||||
completionCounter += 1
|
||||
}
|
||||
}.toList()
|
||||
val end = Instant.now()
|
||||
log.info {
|
||||
val elapsed = Duration.between(start, end).toMillis()
|
||||
"Insertion rate: ${numberOfEntries.toDouble() / elapsed * 1000} ops/s"
|
||||
}
|
||||
inserted
|
||||
}
|
||||
log.info {
|
||||
"Inserted ${entries.size} entries"
|
||||
}
|
||||
if (entries.isNotEmpty()) {
|
||||
val completionQueue = LinkedBlockingQueue<Future<Unit>>(entries.size)
|
||||
val start = Instant.now()
|
||||
entries.forEach { entry ->
|
||||
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 { _, _ ->
|
||||
completionQueue.put(future)
|
||||
}
|
||||
}
|
||||
var completionCounter = 0
|
||||
while (completionCounter < entries.size) {
|
||||
completionQueue.take()
|
||||
completionCounter += 1
|
||||
}
|
||||
val end = Instant.now()
|
||||
log.info {
|
||||
val elapsed = Duration.between(start, end).toMillis()
|
||||
"Retrieval rate: ${entries.size.toDouble() / elapsed * 1000} ops/s"
|
||||
}
|
||||
} else {
|
||||
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
package net.woggioni.gbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.gbcs.GradleBuildCacheServer
|
||||
import net.woggioni.gbcs.GradleBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
|
||||
import net.woggioni.gbcs.base.contextLogger
|
||||
import net.woggioni.gbcs.base.debug
|
||||
import net.woggioni.gbcs.base.info
|
||||
import net.woggioni.gbcs.cli.impl.GbcsCommand
|
||||
import net.woggioni.gbcs.client.GbcsClient
|
||||
import net.woggioni.jwo.Application
|
||||
import net.woggioni.jwo.JWO
|
||||
import picocli.CommandLine
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "server",
|
||||
description = ["GBCS server"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class ServerCommand(app : Application) : GbcsCommand() {
|
||||
|
||||
private val log = contextLogger()
|
||||
|
||||
private fun createDefaultConfigurationFile(configurationFile: Path) {
|
||||
log.info {
|
||||
"Creating default configuration file at '$configurationFile'"
|
||||
}
|
||||
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
|
||||
Files.newOutputStream(configurationFile).use { outputStream ->
|
||||
defaultConfigurationFileResource.openStream().use { inputStream ->
|
||||
JWO.copy(inputStream, outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-c", "--config-file"],
|
||||
description = ["Read the application configuration from this file"],
|
||||
paramLabel = "CONFIG_FILE"
|
||||
)
|
||||
private var configurationFile: Path = findConfigurationFile(app, "gbcs-server.xml")
|
||||
|
||||
val configuration : GbcsClient.Configuration by lazy {
|
||||
GbcsClient.Configuration.parse(configurationFile)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (!Files.exists(configurationFile)) {
|
||||
Files.createDirectories(configurationFile.parent)
|
||||
createDefaultConfigurationFile(configurationFile)
|
||||
}
|
||||
|
||||
val configuration = GradleBuildCacheServer.loadConfiguration(configurationFile)
|
||||
log.debug {
|
||||
ByteArrayOutputStream().also {
|
||||
GradleBuildCacheServer.dumpConfiguration(configuration, it)
|
||||
}.let {
|
||||
"Server configuration:\n${String(it.toByteArray())}"
|
||||
}
|
||||
}
|
||||
val server = GradleBuildCacheServer(configuration)
|
||||
server.run().use {
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
alias catalog.plugins.kotlin.jvm
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':gbcs-base')
|
||||
implementation catalog.picocli
|
||||
implementation catalog.slf4j.api
|
||||
implementation catalog.netty.buffer
|
||||
implementation catalog.netty.codec.http
|
||||
}
|
||||
|
||||
|
@@ -1,262 +0,0 @@
|
||||
package net.woggioni.gbcs.benchmark;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import net.woggioni.jwo.Fun;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Level;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.TearDown;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class Main {
|
||||
|
||||
@SneakyThrows
|
||||
private static Properties loadProperties() {
|
||||
Properties properties = new Properties();
|
||||
try (final var is = Main.class.getResourceAsStream("/benchmark.properties")) {
|
||||
properties.load(is);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static final Properties properties = loadProperties();
|
||||
|
||||
@State(Scope.Thread)
|
||||
public static class ExecutionPlan {
|
||||
private final Random random = new Random(101325);
|
||||
|
||||
@Getter
|
||||
private final HttpClient client = createHttpClient();
|
||||
|
||||
private final Map<String, byte[]> entries = new HashMap<>();
|
||||
|
||||
|
||||
private HttpClient createHttpClient() {
|
||||
final var clientBuilder = HttpClient.newBuilder();
|
||||
getSslContext().ifPresent(clientBuilder::sslContext);
|
||||
return clientBuilder.build();
|
||||
}
|
||||
|
||||
public final Map<String, byte[]> getEntries() {
|
||||
return Collections.unmodifiableMap(entries);
|
||||
}
|
||||
|
||||
public Map.Entry<String, byte[]> newEntry() {
|
||||
final var keyBuffer = new byte[0x20];
|
||||
random.nextBytes(keyBuffer);
|
||||
final var key = Base64.getUrlEncoder().encodeToString(keyBuffer);
|
||||
final var value = new byte[0x1000];
|
||||
random.nextBytes(value);
|
||||
return Map.entry(key, value);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public HttpRequest.Builder newRequestBuilder(String key) {
|
||||
final var requestBuilder = HttpRequest.newBuilder()
|
||||
.uri(getServerURI().resolve(key));
|
||||
String user = getUser();
|
||||
if (user != null) {
|
||||
requestBuilder.header("Authorization", buildAuthorizationHeader(user, getPassword()));
|
||||
}
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public URI getServerURI() {
|
||||
return new URI(properties.getProperty("gbcs.server.url"));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Optional<String> getClientTrustStorePassword() {
|
||||
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.password"))
|
||||
.filter(Predicate.not(String::isEmpty));
|
||||
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Optional<KeyStore> getClientTrustStore() {
|
||||
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.file"))
|
||||
.filter(Predicate.not(String::isEmpty))
|
||||
.map(Path::of)
|
||||
.map((Fun<Path, KeyStore>) keyStoreFile -> {
|
||||
final var keyStore = KeyStore.getInstance("PKCS12");
|
||||
try (final var is = Files.newInputStream(keyStoreFile)) {
|
||||
keyStore.load(is, getClientTrustStorePassword().map(String::toCharArray).orElse(null));
|
||||
}
|
||||
return keyStore;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Optional<KeyStore> getClientKeyStore() {
|
||||
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.file"))
|
||||
.filter(Predicate.not(String::isEmpty))
|
||||
.map(Path::of)
|
||||
.map((Fun<Path, KeyStore>) keyStoreFile -> {
|
||||
final var keyStore = KeyStore.getInstance("PKCS12");
|
||||
try (final var is = Files.newInputStream(keyStoreFile)) {
|
||||
keyStore.load(is, getClientKeyStorePassword().map(String::toCharArray).orElse(null));
|
||||
}
|
||||
return keyStore;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Optional<String> getClientKeyStorePassword() {
|
||||
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.password"))
|
||||
.filter(Predicate.not(String::isEmpty));
|
||||
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Optional<String> getClientKeyPassword() {
|
||||
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.key.password"))
|
||||
.filter(Predicate.not(String::isEmpty));
|
||||
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public String getUser() {
|
||||
return Optional.ofNullable(properties.getProperty("gbcs.server.username"))
|
||||
.filter(Predicate.not(String::isEmpty))
|
||||
.orElse(null);
|
||||
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public String getPassword() {
|
||||
return Optional.ofNullable(properties.getProperty("gbcs.server.password"))
|
||||
.filter(Predicate.not(String::isEmpty))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String buildAuthorizationHeader(String user, String password) {
|
||||
final var b64 = Base64.getEncoder().encode(String.format("%s:%s", user, password).getBytes(StandardCharsets.UTF_8));
|
||||
return "Basic " + new String(b64);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private Optional<SSLContext> getSslContext() {
|
||||
return getClientKeyStore().map((Fun<KeyStore, SSLContext>) clientKeyStore -> {
|
||||
final var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(clientKeyStore, getClientKeyStorePassword().map(String::toCharArray).orElse(null));
|
||||
|
||||
|
||||
// Set up trust manager factory with the truststore
|
||||
final var trustManagers = getClientTrustStore().map((Fun<KeyStore, TrustManager[]>) ts -> {
|
||||
final var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ts);
|
||||
return tmf.getTrustManagers();
|
||||
}).orElse(new TrustManager[0]);
|
||||
|
||||
// Create SSL context with the key and trust managers
|
||||
final var sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(kmf.getKeyManagers(), trustManagers, null);
|
||||
return sslContext;
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Setup(Level.Trial)
|
||||
public void setUp() {
|
||||
final var client = getClient();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
final var pair = newEntry();
|
||||
final var requestBuilder = newRequestBuilder(pair.getKey())
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(pair.getValue()));
|
||||
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
|
||||
if (201 != response.statusCode()) {
|
||||
throw new IllegalStateException(Integer.toString(response.statusCode()));
|
||||
} else {
|
||||
entries.put(pair.getKey(), pair.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TearDown
|
||||
public void tearDown() {
|
||||
client.close();
|
||||
}
|
||||
|
||||
|
||||
private Iterator<Map.Entry<String, byte[]>> it = null;
|
||||
|
||||
private Map.Entry<String, byte[]> nextEntry() {
|
||||
if (it == null || !it.hasNext()) {
|
||||
it = getEntries().entrySet().iterator();
|
||||
}
|
||||
return it.next();
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
@OutputTimeUnit(TimeUnit.SECONDS)
|
||||
public void get(ExecutionPlan plan) {
|
||||
final var client = plan.getClient();
|
||||
final var entry = plan.nextEntry();
|
||||
final var requestBuilder = plan.newRequestBuilder(entry.getKey())
|
||||
.header("Accept", "application/octet-stream")
|
||||
.GET();
|
||||
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
|
||||
if (200 != response.statusCode()) {
|
||||
throw new IllegalStateException(Integer.toString(response.statusCode()));
|
||||
} else {
|
||||
if (!Arrays.equals(entry.getValue(), response.body())) {
|
||||
throw new IllegalStateException("Retrieved unexpected value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
@OutputTimeUnit(TimeUnit.SECONDS)
|
||||
public void put(Main.ExecutionPlan plan) {
|
||||
final var client = plan.getClient();
|
||||
final var entry = plan.nextEntry();
|
||||
|
||||
final var requestBuilder = plan.newRequestBuilder(entry.getKey())
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(entry.getValue()));
|
||||
|
||||
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
|
||||
if (201 != response.statusCode()) {
|
||||
throw new IllegalStateException(Integer.toString(response.statusCode()));
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
gbcs.server.url= https://gbcs.woggioni.net:443
|
||||
gbcs.client.ssl.keystore.file=conf/woggioni@c962475fa38.p12
|
||||
gbcs.client.ssl.keystore.password=password
|
||||
gbcs.client.ssl.key.password=password
|
||||
gbcs.client.ssl.truststore.file=conf/truststore.pfx
|
||||
gbcs.client.ssl.truststore.password=password
|
@@ -1,254 +0,0 @@
|
||||
package net.woggioni.gbcs.client
|
||||
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.Channel
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelOption
|
||||
import io.netty.channel.ChannelPipeline
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.channel.pool.AbstractChannelPoolHandler
|
||||
import io.netty.channel.pool.ChannelPool
|
||||
import io.netty.channel.pool.FixedChannelPool
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import io.netty.handler.codec.DecoderException
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest
|
||||
import io.netty.handler.codec.http.FullHttpRequest
|
||||
import io.netty.handler.codec.http.FullHttpResponse
|
||||
import io.netty.handler.codec.http.HttpClientCodec
|
||||
import io.netty.handler.codec.http.HttpContentDecompressor
|
||||
import io.netty.handler.codec.http.HttpHeaderNames
|
||||
import io.netty.handler.codec.http.HttpHeaderValues
|
||||
import io.netty.handler.codec.http.HttpMethod
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import io.netty.handler.codec.http.HttpVersion
|
||||
import io.netty.handler.ssl.SslContext
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import io.netty.handler.stream.ChunkedWriteHandler
|
||||
import io.netty.util.concurrent.Future
|
||||
import io.netty.util.concurrent.GenericFutureListener
|
||||
import net.woggioni.gbcs.base.Xml
|
||||
import net.woggioni.gbcs.base.contextLogger
|
||||
import net.woggioni.gbcs.base.debug
|
||||
import net.woggioni.gbcs.base.info
|
||||
import net.woggioni.gbcs.client.impl.Parser
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Base64
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import io.netty.util.concurrent.Future as NettyFuture
|
||||
|
||||
|
||||
class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable {
|
||||
private val group: NioEventLoopGroup
|
||||
private var sslContext: SslContext
|
||||
private val log = contextLogger()
|
||||
private val pool: ChannelPool
|
||||
|
||||
data class Configuration(
|
||||
val profiles: Map<String, Profile>
|
||||
) {
|
||||
sealed class Authentication {
|
||||
data class TlsClientAuthenticationCredentials(
|
||||
val key: PrivateKey,
|
||||
val certificateChain: Array<X509Certificate>
|
||||
) : Authentication()
|
||||
|
||||
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
|
||||
}
|
||||
|
||||
data class Profile(
|
||||
val serverURI: URI,
|
||||
val authentication: Authentication?,
|
||||
val maxConnections : Int
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun parse(path: Path): Configuration {
|
||||
return Files.newInputStream(path).use {
|
||||
Xml.parseXml(path.toUri().toURL(), it)
|
||||
}.let(Parser::parse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
group = NioEventLoopGroup()
|
||||
sslContext = SslContextBuilder.forClient().also { builder ->
|
||||
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
|
||||
builder.keyManager(
|
||||
tlsClientAuthenticationCredentials.key,
|
||||
*tlsClientAuthenticationCredentials.certificateChain
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
|
||||
val (scheme, host, port) = profile.serverURI.run {
|
||||
Triple(
|
||||
if (scheme == null) "http" else profile.serverURI.scheme,
|
||||
host,
|
||||
port.takeIf { it > 0 } ?: if ("https" == scheme.lowercase()) 443 else 80
|
||||
)
|
||||
}
|
||||
|
||||
val bootstrap = Bootstrap().apply {
|
||||
group(group)
|
||||
channel(NioSocketChannel::class.java)
|
||||
option(ChannelOption.TCP_NODELAY, true)
|
||||
option(ChannelOption.SO_KEEPALIVE, true)
|
||||
remoteAddress(InetSocketAddress(host, port))
|
||||
}
|
||||
val channelPoolHandler = object : AbstractChannelPoolHandler() {
|
||||
|
||||
@Volatile
|
||||
private var connectionCount = AtomicInteger()
|
||||
|
||||
@Volatile
|
||||
private var leaseCount = AtomicInteger()
|
||||
|
||||
override fun channelReleased(ch: Channel) {
|
||||
log.debug {
|
||||
"Released lease ${leaseCount.decrementAndGet()}"
|
||||
}
|
||||
}
|
||||
|
||||
override fun channelAcquired(ch: Channel?) {
|
||||
log.debug {
|
||||
"Acquired lease ${leaseCount.getAndIncrement()}"
|
||||
}
|
||||
}
|
||||
|
||||
override fun channelCreated(ch: Channel) {
|
||||
log.debug {
|
||||
"Created connection ${connectionCount.getAndIncrement()}"
|
||||
}
|
||||
val pipeline: ChannelPipeline = ch.pipeline()
|
||||
|
||||
// Add SSL handler if needed
|
||||
if ("https".equals(scheme, ignoreCase = true)) {
|
||||
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc(), host, port))
|
||||
}
|
||||
|
||||
// HTTP handlers
|
||||
pipeline.addLast("codec", HttpClientCodec())
|
||||
pipeline.addLast("decompressor", HttpContentDecompressor())
|
||||
pipeline.addLast("aggregator", HttpObjectAggregator(1048576))
|
||||
pipeline.addLast("chunked", ChunkedWriteHandler())
|
||||
}
|
||||
}
|
||||
pool = FixedChannelPool(bootstrap, channelPoolHandler, profile.maxConnections)
|
||||
}
|
||||
|
||||
fun get(key: String): CompletableFuture<ByteArray?> {
|
||||
return sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)
|
||||
.thenApply {
|
||||
val status = it.status()
|
||||
if (it.status() == HttpResponseStatus.NOT_FOUND) {
|
||||
null
|
||||
} else if (it.status() != HttpResponseStatus.OK) {
|
||||
throw HttpException(status)
|
||||
} else {
|
||||
it.content()
|
||||
}
|
||||
}.thenApply { maybeByteBuf ->
|
||||
maybeByteBuf?.let {
|
||||
val result = ByteArray(it.readableBytes())
|
||||
it.getBytes(0, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun put(key: String, content: ByteArray): CompletableFuture<Unit> {
|
||||
return sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content).thenApply {
|
||||
val status = it.status()
|
||||
if (it.status() != HttpResponseStatus.CREATED) {
|
||||
throw HttpException(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?): CompletableFuture<FullHttpResponse> {
|
||||
val responseFuture = CompletableFuture<FullHttpResponse>()
|
||||
// Custom handler for processing responses
|
||||
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
||||
override fun operationComplete(channelFuture: Future<Channel>) {
|
||||
if (channelFuture.isSuccess) {
|
||||
val channel = channelFuture.now
|
||||
val pipeline = channel.pipeline()
|
||||
channel.pipeline().addLast("handler", object : SimpleChannelInboundHandler<FullHttpResponse>() {
|
||||
override fun channelRead0(
|
||||
ctx: ChannelHandlerContext,
|
||||
response: FullHttpResponse
|
||||
) {
|
||||
responseFuture.complete(response)
|
||||
pipeline.removeLast()
|
||||
pool.release(channel)
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
val ex = when (cause) {
|
||||
is DecoderException -> cause.cause
|
||||
else -> cause
|
||||
}
|
||||
responseFuture.completeExceptionally(ex)
|
||||
ctx.close()
|
||||
pipeline.removeLast()
|
||||
pool.release(channel)
|
||||
}
|
||||
})
|
||||
// Prepare the HTTP request
|
||||
val request: FullHttpRequest = let {
|
||||
val content: ByteBuf? = body?.takeIf(ByteArray::isNotEmpty)?.let(Unpooled::wrappedBuffer)
|
||||
DefaultFullHttpRequest(
|
||||
HttpVersion.HTTP_1_1,
|
||||
method,
|
||||
uri.rawPath,
|
||||
content ?: Unpooled.buffer(0)
|
||||
).apply {
|
||||
headers().apply {
|
||||
if (content != null) {
|
||||
set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM)
|
||||
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
|
||||
}
|
||||
set(HttpHeaderNames.HOST, profile.serverURI.host)
|
||||
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
||||
set(
|
||||
HttpHeaderNames.ACCEPT_ENCODING,
|
||||
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
|
||||
)
|
||||
// Add basic auth if configured
|
||||
(profile.authentication as? Configuration.Authentication.BasicAuthenticationCredentials)?.let { credentials ->
|
||||
val auth = "${credentials.username}:${credentials.password}"
|
||||
val encodedAuth = Base64.getEncoder().encodeToString(auth.toByteArray())
|
||||
set(HttpHeaderNames.AUTHORIZATION, "Basic $encodedAuth")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set headers
|
||||
// Send the request
|
||||
channel.writeAndFlush(request)
|
||||
}
|
||||
}
|
||||
})
|
||||
return responseFuture
|
||||
}
|
||||
|
||||
fun shutDown(): NettyFuture<*> {
|
||||
return group.shutdownGracefully()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
shutDown().sync()
|
||||
}
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
package net.woggioni.gbcs.client.impl
|
||||
|
||||
import net.woggioni.gbcs.base.Xml.Companion.asIterable
|
||||
import net.woggioni.gbcs.client.GbcsClient
|
||||
import org.w3c.dom.Document
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
object Parser {
|
||||
|
||||
fun parse(document: Document): GbcsClient.Configuration {
|
||||
val root = document.documentElement
|
||||
|
||||
val profiles = mutableMapOf<String, GbcsClient.Configuration.Profile>()
|
||||
|
||||
for (child in root.asIterable()) {
|
||||
val tagName = child.localName
|
||||
when (tagName) {
|
||||
"profile" -> {
|
||||
val name = child.getAttribute("name")
|
||||
val uri = child.getAttribute("base-url").let(::URI)
|
||||
var authentication: GbcsClient.Configuration.Authentication? = null
|
||||
for (gchild in child.asIterable()) {
|
||||
when (gchild.localName) {
|
||||
"tls-client-auth" -> {
|
||||
val keyStoreFile = gchild.getAttribute("key-store-file")
|
||||
val keyStorePassword =
|
||||
gchild.getAttribute("key-store-password").takeIf(String::isNotEmpty)
|
||||
val keyAlias = gchild.getAttribute("key-alias")
|
||||
val keyPassword = gchild.getAttribute("key-password").takeIf(String::isNotEmpty)
|
||||
|
||||
val keystore = KeyStore.getInstance("PKCS12").apply {
|
||||
Files.newInputStream(Path.of(keyStoreFile)).use {
|
||||
load(it, keyStorePassword?.toCharArray())
|
||||
}
|
||||
}
|
||||
val key = keystore.getKey(keyAlias, keyPassword?.toCharArray()) as PrivateKey
|
||||
val certChain = keystore.getCertificateChain(keyAlias).asSequence()
|
||||
.map { it as X509Certificate }
|
||||
.toList()
|
||||
.toTypedArray()
|
||||
authentication =
|
||||
GbcsClient.Configuration.Authentication.TlsClientAuthenticationCredentials(key, certChain)
|
||||
}
|
||||
|
||||
"basic-auth" -> {
|
||||
val username = gchild.getAttribute("user")
|
||||
val password = gchild.getAttribute("password")
|
||||
authentication =
|
||||
GbcsClient.Configuration.Authentication.BasicAuthenticationCredentials(username, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
val maxConnections = child.getAttribute("max-connections")
|
||||
.takeIf(String::isNotEmpty)
|
||||
?.let(String::toInt)
|
||||
?: 50
|
||||
profiles[name] = GbcsClient.Configuration.Profile(uri, authentication, maxConnections)
|
||||
}
|
||||
}
|
||||
}
|
||||
return GbcsClient.Configuration(profiles)
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<xs:schema targetNamespace="urn:net.woggioni.gbcs.client"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
|
||||
elementFormDefault="unqualified"
|
||||
>
|
||||
<xs:element name="profiles" type="gbcs-client:profilesType"/>
|
||||
|
||||
<xs:complexType name="profilesType">
|
||||
<xs:sequence minOccurs="0">
|
||||
<xs:element name="profile" type="gbcs-client:profileType" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="profileType">
|
||||
<xs:choice>
|
||||
<xs:element name="basic-auth" type="gbcs-client:basicAuthType"/>
|
||||
<xs:element name="tls-client-auth" type="gbcs-client:tlsClientAuthType"/>
|
||||
</xs:choice>
|
||||
<xs:attribute name="name" type="xs:token" use="required"/>
|
||||
<xs:attribute name="base-url" type="xs:anyURI" use="required"/>
|
||||
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="basicAuthType">
|
||||
<xs:attribute name="user" type="xs:string" use="required"/>
|
||||
<xs:attribute name="password" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="tlsClientAuthType">
|
||||
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/>
|
||||
<xs:attribute name="key-store-password" type="xs:string" use="required"/>
|
||||
<xs:attribute name="key-alias" type="xs:string" use="required"/>
|
||||
<xs:attribute name="key-password" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<gbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
|
||||
xs:schemaLocation="urn:net.woggioni.gbcs.client jms://net.woggioni.gbcs.client/net/woggioni/gbcs/client/schema/gbcs-client.xsd"
|
||||
>
|
||||
<profile name="profile1" base-url="https://gbcs1.example.com/">
|
||||
<tls-client-auth
|
||||
key-store-file="keystore.pfx"
|
||||
key-store-password="password"
|
||||
key-alias="woggioni@c962475fa38"
|
||||
key-password="key-password"/>
|
||||
</profile>
|
||||
<profile name="profile2" base-url="https://gbcs2.example.com/">
|
||||
<basic-auth user="user" password="password"/>
|
||||
</profile>
|
||||
</gbcs-client:profiles>
|
@@ -1,14 +0,0 @@
|
||||
import net.woggioni.gbcs.api.CacheProvider;
|
||||
|
||||
module net.woggioni.gbcs.memcached {
|
||||
requires net.woggioni.gbcs.base;
|
||||
requires net.woggioni.gbcs.api;
|
||||
requires com.googlecode.xmemcached;
|
||||
requires net.woggioni.jwo;
|
||||
requires java.xml;
|
||||
requires kotlin.stdlib;
|
||||
|
||||
provides CacheProvider with net.woggioni.gbcs.memcached.MemcachedCacheProvider;
|
||||
|
||||
opens net.woggioni.gbcs.memcached.schema;
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
package net.woggioni.gbcs.memcached
|
||||
|
||||
import net.rubyeye.xmemcached.XMemcachedClientBuilder
|
||||
import net.rubyeye.xmemcached.command.BinaryCommandFactory
|
||||
import net.rubyeye.xmemcached.transcoders.CompressionMode
|
||||
import net.rubyeye.xmemcached.transcoders.SerializingTranscoder
|
||||
import net.woggioni.gbcs.api.Cache
|
||||
import net.woggioni.gbcs.api.exception.ContentTooLargeException
|
||||
import net.woggioni.gbcs.base.HostAndPort
|
||||
import net.woggioni.jwo.JWO
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.ReadableByteChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.time.Duration
|
||||
|
||||
class MemcachedCache(
|
||||
servers: List<HostAndPort>,
|
||||
private val maxAge: Duration,
|
||||
maxSize : Int,
|
||||
digestAlgorithm: String?,
|
||||
compressionMode: CompressionMode,
|
||||
) : Cache {
|
||||
private val memcachedClient = XMemcachedClientBuilder(
|
||||
servers.stream().map { addr: HostAndPort -> InetSocketAddress(addr.host, addr.port) }.toList()
|
||||
).apply {
|
||||
commandFactory = BinaryCommandFactory()
|
||||
digestAlgorithm?.let { dAlg ->
|
||||
setKeyProvider { key ->
|
||||
val md = MessageDigest.getInstance(dAlg)
|
||||
md.update(key.toByteArray(StandardCharsets.UTF_8))
|
||||
JWO.bytesToHex(md.digest())
|
||||
}
|
||||
}
|
||||
transcoder = SerializingTranscoder(maxSize).apply {
|
||||
setCompressionMode(compressionMode)
|
||||
}
|
||||
}.build()
|
||||
|
||||
override fun get(key: String): ReadableByteChannel? {
|
||||
return memcachedClient.get<ByteArray>(key)
|
||||
?.let(::ByteArrayInputStream)
|
||||
?.let(Channels::newChannel)
|
||||
}
|
||||
|
||||
override fun put(key: String, content: ByteArray) {
|
||||
try {
|
||||
memcachedClient[key, maxAge.toSeconds().toInt()] = content
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw ContentTooLargeException(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
memcachedClient.shutdown()
|
||||
}
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
package net.woggioni.gbcs.memcached
|
||||
|
||||
import net.rubyeye.xmemcached.transcoders.CompressionMode
|
||||
import net.woggioni.gbcs.api.Configuration
|
||||
import net.woggioni.gbcs.base.HostAndPort
|
||||
import java.time.Duration
|
||||
|
||||
data class MemcachedCacheConfiguration(
|
||||
var servers: List<HostAndPort>,
|
||||
var maxAge: Duration = Duration.ofDays(1),
|
||||
var maxSize: Int = 0x100000,
|
||||
var digestAlgorithm: String? = null,
|
||||
var compressionMode: CompressionMode = CompressionMode.ZIP,
|
||||
) : Configuration.Cache {
|
||||
override fun materialize() = MemcachedCache(
|
||||
servers,
|
||||
maxAge,
|
||||
maxSize,
|
||||
digestAlgorithm,
|
||||
compressionMode
|
||||
)
|
||||
|
||||
override fun getNamespaceURI() = "urn:net.woggioni.gbcs-memcached"
|
||||
|
||||
override fun getTypeName() = "memcachedCacheType"
|
||||
}
|
@@ -1,91 +0,0 @@
|
||||
package net.woggioni.gbcs.memcached
|
||||
|
||||
import net.rubyeye.xmemcached.transcoders.CompressionMode
|
||||
import net.woggioni.gbcs.api.CacheProvider
|
||||
import net.woggioni.gbcs.base.GBCS
|
||||
import net.woggioni.gbcs.base.HostAndPort
|
||||
import net.woggioni.gbcs.base.Xml
|
||||
import net.woggioni.gbcs.base.Xml.Companion.asIterable
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.time.Duration
|
||||
|
||||
class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
|
||||
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd"
|
||||
|
||||
override fun getXmlType() = "memcachedCacheType"
|
||||
|
||||
override fun getXmlNamespace() = "urn:net.woggioni.gbcs-memcached"
|
||||
|
||||
val xmlNamespacePrefix : String
|
||||
get() = "gbcs-memcached"
|
||||
|
||||
override fun deserialize(el: Element): MemcachedCacheConfiguration {
|
||||
val servers = mutableListOf<HostAndPort>()
|
||||
val maxAge = el.getAttribute("max-age")
|
||||
.takeIf(String::isNotEmpty)
|
||||
?.let(Duration::parse)
|
||||
?: Duration.ofDays(1)
|
||||
val maxSize = el.getAttribute("max-size")
|
||||
.takeIf(String::isNotEmpty)
|
||||
?.let(String::toInt)
|
||||
?: 0x100000
|
||||
val enableCompression = el.getAttribute("enable-compression")
|
||||
.takeIf(String::isNotEmpty)
|
||||
?.let(String::toBoolean)
|
||||
?: false
|
||||
val compressionMode = el.getAttribute("compression-mode")
|
||||
.takeIf(String::isNotEmpty)
|
||||
?.let {
|
||||
when (it) {
|
||||
"gzip" -> CompressionMode.GZIP
|
||||
"zip" -> CompressionMode.ZIP
|
||||
else -> CompressionMode.ZIP
|
||||
}
|
||||
}
|
||||
?: CompressionMode.ZIP
|
||||
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty)
|
||||
for (child in el.asIterable()) {
|
||||
when (child.nodeName) {
|
||||
"server" -> {
|
||||
servers.add(HostAndPort(child.getAttribute("host"), child.getAttribute("port").toInt()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MemcachedCacheConfiguration(
|
||||
servers,
|
||||
maxAge,
|
||||
maxSize,
|
||||
digestAlgorithm,
|
||||
compressionMode,
|
||||
)
|
||||
}
|
||||
|
||||
override fun serialize(doc: Document, cache: MemcachedCacheConfiguration) = cache.run {
|
||||
val result = doc.createElement("cache")
|
||||
Xml.of(doc, result) {
|
||||
attr("xmlns:${xmlNamespacePrefix}", xmlNamespace, namespaceURI = "http://www.w3.org/2000/xmlns/")
|
||||
|
||||
attr("xs:type", "${xmlNamespacePrefix}:$xmlType", GBCS.XML_SCHEMA_NAMESPACE_URI)
|
||||
for (server in servers) {
|
||||
node("server") {
|
||||
attr("host", server.host)
|
||||
attr("port", server.port.toString())
|
||||
}
|
||||
}
|
||||
attr("max-age", maxAge.toString())
|
||||
attr("max-size", maxSize.toString())
|
||||
digestAlgorithm?.let { digestAlgorithm ->
|
||||
attr("digest", digestAlgorithm)
|
||||
}
|
||||
attr(
|
||||
"compression-mode", when (compressionMode) {
|
||||
CompressionMode.GZIP -> "gzip"
|
||||
CompressionMode.ZIP -> "zip"
|
||||
}
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
net.woggioni.gbcs.memcached.MemcachedCacheProvider
|
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<xs:schema targetNamespace="urn:net.woggioni.gbcs-memcached"
|
||||
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached"
|
||||
xmlns:gbcs="urn:net.woggioni.gbcs"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
|
||||
<xs:import schemaLocation="classpath:net/woggioni/gbcs/schema/gbcs.xsd" namespace="urn:net.woggioni.gbcs"/>
|
||||
|
||||
<xs:complexType name="memcachedServerType">
|
||||
<xs:attribute name="host" type="xs:string" use="required"/>
|
||||
<xs:attribute name="port" type="xs:positiveInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="memcachedCacheType">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="gbcs:cacheType">
|
||||
<xs:sequence maxOccurs="unbounded">
|
||||
<xs:element name="server" type="gbcs-memcached:memcachedServerType"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
|
||||
<xs:attribute name="max-size" type="xs:unsignedInt" default="1048576"/>
|
||||
<xs:attribute name="digest" type="xs:token" />
|
||||
<xs:attribute name="compression-mode" type="gbcs-memcached:compressionType" default="zip"/>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="compressionType">
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:enumeration value="zip"/>
|
||||
<xs:enumeration value="gzip"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
</xs:schema>
|
@@ -2,9 +2,10 @@ org.gradle.configuration-cache=false
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
gbcs.version = 0.0.1
|
||||
rbcs.version = 0.2.0
|
||||
|
||||
lys.version = 2025.01.10
|
||||
lys.version = 2025.03.03
|
||||
|
||||
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
||||
docker.registry.url=gitea.woggioni.net
|
||||
|
||||
|
@@ -5,6 +5,9 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api catalog.netty.common
|
||||
api catalog.netty.buffer
|
||||
api catalog.netty.handler
|
||||
}
|
||||
|
||||
publishing {
|
11
rbcs-api/src/main/java/module-info.java
Normal file
11
rbcs-api/src/main/java/module-info.java
Normal file
@@ -0,0 +1,11 @@
|
||||
module net.woggioni.rbcs.api {
|
||||
requires static lombok;
|
||||
requires java.xml;
|
||||
requires io.netty.buffer;
|
||||
requires io.netty.handler;
|
||||
requires io.netty.transport;
|
||||
requires io.netty.common;
|
||||
exports net.woggioni.rbcs.api;
|
||||
exports net.woggioni.rbcs.api.exception;
|
||||
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,15 @@
|
||||
package net.woggioni.rbcs.api;
|
||||
|
||||
import io.netty.channel.ChannelFactory;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
|
||||
public interface CacheHandlerFactory extends AsyncCloseable {
|
||||
ChannelHandler newHandler(
|
||||
EventLoopGroup eventLoopGroup,
|
||||
ChannelFactory<SocketChannel> socketChannelFactory,
|
||||
ChannelFactory<DatagramChannel> datagramChannelFactory
|
||||
);
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
package net.woggioni.rbcs.api;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
@@ -0,0 +1,14 @@
|
||||
package net.woggioni.rbcs.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class CacheValueMetadata implements Serializable {
|
||||
private final String contentDisposition;
|
||||
private final String mimeType;
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
package net.woggioni.gbcs.api;
|
||||
package net.woggioni.rbcs.api;
|
||||
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
import lombok.Value;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -14,19 +16,46 @@ import java.util.stream.Collectors;
|
||||
public class Configuration {
|
||||
String host;
|
||||
int port;
|
||||
int incomingConnectionsBacklogSize;
|
||||
String serverPath;
|
||||
@NonNull
|
||||
EventExecutor eventExecutor;
|
||||
@NonNull
|
||||
Connection connection;
|
||||
Map<String, User> users;
|
||||
Map<String, Group> groups;
|
||||
Cache cache;
|
||||
Authentication authentication;
|
||||
Tls tls;
|
||||
boolean useVirtualThread;
|
||||
|
||||
@Value
|
||||
public static class EventExecutor {
|
||||
boolean useVirtualThreads;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class Connection {
|
||||
Duration idleTimeout;
|
||||
Duration readIdleTimeout;
|
||||
Duration writeIdleTimeout;
|
||||
int maxRequestSize;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class Quota {
|
||||
long calls;
|
||||
Duration period;
|
||||
long initialAvailableCalls;
|
||||
long maxAvailableCalls;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class Group {
|
||||
@EqualsAndHashCode.Include
|
||||
String name;
|
||||
Set<Role> roles;
|
||||
Quota groupQuota;
|
||||
Quota userQuota;
|
||||
}
|
||||
|
||||
@Value
|
||||
@@ -35,7 +64,7 @@ public class Configuration {
|
||||
String name;
|
||||
String password;
|
||||
Set<Group> groups;
|
||||
|
||||
Quota quota;
|
||||
|
||||
public Set<Role> getRoles() {
|
||||
return groups.stream()
|
||||
@@ -58,7 +87,6 @@ public class Configuration {
|
||||
public static class Tls {
|
||||
KeyStore keyStore;
|
||||
TrustStore trustStore;
|
||||
boolean verifyClients;
|
||||
}
|
||||
|
||||
@Value
|
||||
@@ -74,6 +102,7 @@ public class Configuration {
|
||||
Path file;
|
||||
String password;
|
||||
boolean checkCertificateStatus;
|
||||
boolean requireClientCertificate;
|
||||
}
|
||||
|
||||
@Value
|
||||
@@ -93,7 +122,7 @@ public class Configuration {
|
||||
}
|
||||
|
||||
public interface Cache {
|
||||
net.woggioni.gbcs.api.Cache materialize();
|
||||
CacheHandlerFactory materialize();
|
||||
String getNamespaceURI();
|
||||
String getTypeName();
|
||||
}
|
||||
@@ -101,24 +130,28 @@ public class Configuration {
|
||||
public static Configuration of(
|
||||
String host,
|
||||
int port,
|
||||
int incomingConnectionsBacklogSize,
|
||||
String serverPath,
|
||||
EventExecutor eventExecutor,
|
||||
Connection connection,
|
||||
Map<String, User> users,
|
||||
Map<String, Group> groups,
|
||||
Cache cache,
|
||||
Authentication authentication,
|
||||
Tls tls,
|
||||
boolean useVirtualThread
|
||||
Tls tls
|
||||
) {
|
||||
return new Configuration(
|
||||
host,
|
||||
port,
|
||||
incomingConnectionsBacklogSize,
|
||||
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
|
||||
eventExecutor,
|
||||
connection,
|
||||
users,
|
||||
groups,
|
||||
cache,
|
||||
authentication,
|
||||
tls,
|
||||
useVirtualThread
|
||||
tls
|
||||
);
|
||||
}
|
||||
}
|
5
rbcs-api/src/main/java/net/woggioni/rbcs/api/Role.java
Normal file
5
rbcs-api/src/main/java/net/woggioni/rbcs/api/Role.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package net.woggioni.rbcs.api;
|
||||
|
||||
public enum Role {
|
||||
Reader, Writer, Healthcheck
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package net.woggioni.rbcs.api.exception;
|
||||
|
||||
public class CacheException extends RbcsException {
|
||||
public CacheException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public CacheException(String message) {
|
||||
this(message, null);
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package net.woggioni.rbcs.api.exception;
|
||||
|
||||
public class ConfigurationException extends RbcsException {
|
||||
public ConfigurationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ConfigurationException(String message) {
|
||||
this(message, null);
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
package net.woggioni.gbcs.api.exception;
|
||||
package net.woggioni.rbcs.api.exception;
|
||||
|
||||
public class ContentTooLargeException extends GbcsException {
|
||||
public class ContentTooLargeException extends RbcsException {
|
||||
public ContentTooLargeException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package net.woggioni.rbcs.api.exception;
|
||||
|
||||
public class RbcsException extends RuntimeException {
|
||||
public RbcsException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -0,0 +1,161 @@
|
||||
package net.woggioni.rbcs.api.message;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufHolder;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata;
|
||||
|
||||
public sealed interface CacheMessage {
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
final class CacheGetRequest implements CacheMessage {
|
||||
private final String key;
|
||||
}
|
||||
|
||||
abstract sealed class CacheGetResponse implements CacheMessage {
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
final class CacheValueFoundResponse extends CacheGetResponse {
|
||||
private final String key;
|
||||
private final CacheValueMetadata metadata;
|
||||
}
|
||||
|
||||
final class CacheValueNotFoundResponse extends CacheGetResponse {
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
final class CachePutRequest implements CacheMessage {
|
||||
private final String key;
|
||||
private final CacheValueMetadata metadata;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
final class CachePutResponse implements CacheMessage {
|
||||
private final String key;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
non-sealed class CacheContent implements CacheMessage, ByteBufHolder {
|
||||
protected final ByteBuf chunk;
|
||||
|
||||
@Override
|
||||
public ByteBuf content() {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent copy() {
|
||||
return replace(chunk.copy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent duplicate() {
|
||||
return new CacheContent(chunk.duplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent retainedDuplicate() {
|
||||
return new CacheContent(chunk.retainedDuplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent replace(ByteBuf content) {
|
||||
return new CacheContent(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent retain() {
|
||||
chunk.retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent retain(int increment) {
|
||||
chunk.retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent touch() {
|
||||
chunk.touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheContent touch(Object hint) {
|
||||
chunk.touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int refCnt() {
|
||||
return chunk.refCnt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release() {
|
||||
return chunk.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release(int decrement) {
|
||||
return chunk.release(decrement);
|
||||
}
|
||||
}
|
||||
|
||||
final class LastCacheContent extends CacheContent {
|
||||
public LastCacheContent(ByteBuf chunk) {
|
||||
super(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent copy() {
|
||||
return replace(chunk.copy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent duplicate() {
|
||||
return new LastCacheContent(chunk.duplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent retainedDuplicate() {
|
||||
return new LastCacheContent(chunk.retainedDuplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent replace(ByteBuf content) {
|
||||
return new LastCacheContent(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent retain() {
|
||||
super.retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent retain(int increment) {
|
||||
super.retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent touch() {
|
||||
super.touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LastCacheContent touch(Object hint) {
|
||||
super.touch(hint);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
156
rbcs-cli/build.gradle
Normal file
156
rbcs-cli/build.gradle
Normal file
@@ -0,0 +1,156 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
alias catalog.plugins.kotlin.jvm
|
||||
alias catalog.plugins.envelope
|
||||
alias catalog.plugins.sambal
|
||||
alias catalog.plugins.graalvm.native.image
|
||||
alias catalog.plugins.graalvm.jlink
|
||||
alias catalog.plugins.jpms.check
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
import net.woggioni.gradle.envelope.EnvelopePlugin
|
||||
import net.woggioni.gradle.envelope.EnvelopeJarTask
|
||||
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
|
||||
import net.woggioni.gradle.graalvm.NativeImageTask
|
||||
import net.woggioni.gradle.graalvm.NativeImagePlugin
|
||||
import net.woggioni.gradle.graalvm.UpxTask
|
||||
import net.woggioni.gradle.graalvm.JlinkPlugin
|
||||
import net.woggioni.gradle.graalvm.JlinkTask
|
||||
|
||||
sourceSets {
|
||||
configureNativeImage {
|
||||
java {
|
||||
}
|
||||
kotlin {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
release {
|
||||
transitive = false
|
||||
canBeConsumed = true
|
||||
canBeResolved = true
|
||||
visible = true
|
||||
}
|
||||
|
||||
configureNativeImageImplementation {
|
||||
extendsFrom implementation
|
||||
}
|
||||
|
||||
configureNativeImageRuntimeOnly {
|
||||
extendsFrom runtimeOnly
|
||||
}
|
||||
|
||||
nativeImage {
|
||||
extendsFrom runtimeClasspath
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
configureNativeImageImplementation project
|
||||
configureNativeImageImplementation project(':rbcs-server-memcache')
|
||||
|
||||
implementation catalog.jwo
|
||||
implementation catalog.slf4j.api
|
||||
implementation catalog.picocli
|
||||
|
||||
implementation project(':rbcs-client')
|
||||
implementation project(':rbcs-server')
|
||||
|
||||
// runtimeOnly catalog.slf4j.jdk14
|
||||
runtimeOnly catalog.logback.classic
|
||||
// runtimeOnly catalog.slf4j.simple
|
||||
nativeImage project(':rbcs-server-memcache')
|
||||
|
||||
}
|
||||
|
||||
|
||||
Property<String> mainModuleName = objects.property(String.class)
|
||||
mainModuleName.set('net.woggioni.rbcs.cli')
|
||||
Property<String> mainClassName = objects.property(String.class)
|
||||
mainClassName.set('net.woggioni.rbcs.cli.RemoteBuildCacheServerCli')
|
||||
|
||||
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
|
||||
options.javaModuleMainClass = mainClassName
|
||||
}
|
||||
|
||||
Provider<Jar> jarTaskProvider = tasks.named(JavaPlugin.JAR_TASK_NAME, Jar)
|
||||
|
||||
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.ENVELOPE_JAR_TASK_NAME, EnvelopeJarTask.class) {
|
||||
mainModule = mainModuleName
|
||||
mainClass = mainClassName
|
||||
|
||||
extraClasspath = ["plugins"]
|
||||
|
||||
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/rbcs/cli/logback.xml'
|
||||
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED'
|
||||
}
|
||||
|
||||
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
|
||||
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
|
||||
}
|
||||
|
||||
nativeImage {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(23)
|
||||
vendor = JvmVendorSpec.GRAAL_VM
|
||||
}
|
||||
mainClass = mainClassName
|
||||
// mainModule = mainModuleName
|
||||
useMusl = true
|
||||
buildStaticImage = true
|
||||
linkAtBuildTime = false
|
||||
classpath = project.files(jarTaskProvider, configurations.nativeImage)
|
||||
compressExecutable = true
|
||||
compressionLevel = 10
|
||||
useLZMA = false
|
||||
}
|
||||
|
||||
Provider<UpxTask> upxTaskProvider = tasks.named(NativeImagePlugin.UPX_TASK_NAME, UpxTask) {
|
||||
}
|
||||
|
||||
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
|
||||
mainClass = mainClassName
|
||||
mainModule = 'net.woggioni.rbcs.cli'
|
||||
}
|
||||
|
||||
tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) {
|
||||
from(rootProject.file('conf')) {
|
||||
into('net/woggioni/rbcs/cli')
|
||||
include 'logback.xml'
|
||||
include 'logging.properties'
|
||||
}
|
||||
}
|
||||
|
||||
artifacts {
|
||||
release(envelopeJarTaskProvider)
|
||||
release(upxTaskProvider)
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
artifact envelopeJar
|
||||
artifact(upxTaskProvider) {
|
||||
classifier = "linux-x86_64"
|
||||
extension = "exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
15
rbcs-cli/conf/rbcs-client.xml
Normal file
15
rbcs-cli/conf/rbcs-client.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<rbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:rbcs-client="urn:net.woggioni.rbcs.client"
|
||||
xs:schemaLocation="urn:net.woggioni.rbcs.client jpms://net.woggioni.rbcs.client/net/woggioni/rbcs/client/schema/rbcs-client.xsd"
|
||||
>
|
||||
<profile name="profile1" base-url="https://rbcs1.example.com/">
|
||||
<no-auth/>
|
||||
<connection write-idle-timeout="PT60S"
|
||||
read-idle-timeout="PT60S"
|
||||
idle-timeout="PT30S" />
|
||||
</profile>
|
||||
<profile name="profile2" base-url="https://rbcs2.example.com/">
|
||||
<basic-auth user="user" password="password"/>
|
||||
</profile>
|
||||
</rbcs-client:profiles>
|
53
rbcs-cli/conf/rbcs-server.xml
Normal file
53
rbcs-cli/conf/rbcs-server.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<rbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:rbcs="urn:net.woggioni.rbcs.server"
|
||||
xmlns:rbcs-memcache="urn:net.woggioni.rbcs.server.memcache"
|
||||
xs:schemaLocation="urn:net.woggioni.rbcs.server.memcache jpms://net.woggioni.rbcs.server.memcache/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd"
|
||||
>
|
||||
<bind host="127.0.0.1" port="8080" incoming-connections-backlog-size="1024"/>
|
||||
<connection
|
||||
max-request-size="67108864"
|
||||
idle-timeout="PT10S"
|
||||
read-idle-timeout="PT20S"
|
||||
write-idle-timeout="PT20S"/>
|
||||
<event-executor use-virtual-threads="true"/>
|
||||
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="0x1000" digest="MD5">
|
||||
<server host="127.0.0.1" port="11211" max-connections="256"/>
|
||||
</cache>
|
||||
<!--cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" /-->
|
||||
<!--cache xs:type="rbcs:fileSystemCacheType" max-age="P7D" enable-compression="false" /-->
|
||||
<authorization>
|
||||
<users>
|
||||
<user name="woggioni" password="II+qeNLft2pZ/JVNo9F7jpjM/BqEcfsJW27NZ6dPVs8tAwHbxrJppKYsbL7J/SMl">
|
||||
<quota calls="100" period="PT1S"/>
|
||||
</user>
|
||||
<user name="gitea" password="v6T9+q6/VNpvLknji3ixPiyz2YZCQMXj2FN7hvzbfc2Ig+IzAHO0iiBCH9oWuBDq"/>
|
||||
<anonymous>
|
||||
<quota calls="10" period="PT60S" initial-available-calls="10" max-available-calls="10"/>
|
||||
</anonymous>
|
||||
</users>
|
||||
<groups>
|
||||
<group name="readers">
|
||||
<users>
|
||||
<anonymous/>
|
||||
</users>
|
||||
<roles>
|
||||
<reader/>
|
||||
</roles>
|
||||
</group>
|
||||
<group name="writers">
|
||||
<users>
|
||||
<user ref="woggioni"/>
|
||||
<user ref="gitea"/>
|
||||
</users>
|
||||
<roles>
|
||||
<reader/>
|
||||
<writer/>
|
||||
</roles>
|
||||
</group>
|
||||
</groups>
|
||||
</authorization>
|
||||
<authentication>
|
||||
<none/>
|
||||
</authentication>
|
||||
</rbcs:server>
|
6
rbcs-cli/native-image/jni-config.json
Normal file
6
rbcs-cli/native-image/jni-config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name":"java.lang.Boolean",
|
||||
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
|
||||
}
|
||||
]
|
2
rbcs-cli/native-image/native-image.properties
Normal file
2
rbcs-cli/native-image/native-image.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
Args=-O3 -march=skylake --gc=serial --install-exit-handlers --initialize-at-run-time=io.netty --enable-url-protocols=jpms --initialize-at-build-time=net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory,net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory$JpmsHandler
|
||||
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils
|
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 @@
|
||||
[
|
||||
]
|
756
rbcs-cli/native-image/reflect-config.json
Normal file
756
rbcs-cli/native-image/reflect-config.json
Normal file
@@ -0,0 +1,756 @@
|
||||
[
|
||||
{
|
||||
"name":"android.os.Build$VERSION"
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder",
|
||||
"queryAllPublicMethods":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.classic.joran.SerializedModelConfigurator",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.classic.util.DefaultJoranConfigurator",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.core.ConsoleAppender",
|
||||
"queryAllPublicMethods":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"setTarget","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.core.OutputStreamAppender",
|
||||
"methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.core.encoder.Encoder",
|
||||
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder",
|
||||
"methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase",
|
||||
"methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"ch.qos.logback.core.spi.ContextAware",
|
||||
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"com.aayushatharva.brotli4j.Brotli4jLoader"
|
||||
},
|
||||
{
|
||||
"name":"com.github.luben.zstd.Zstd"
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.AESCipher$General",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.ARCFOURCipher",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.DESCipher",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.DESedeCipher",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.DHParameters",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.HmacCore$HmacSHA512",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA512",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.crypto.provider.TlsMasterSecretGenerator",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.org.apache.xerces.internal.impl.dv.xs.ExtendedSchemaDVFactoryImpl",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.org.apache.xerces.internal.impl.dv.xs.SchemaDVFactoryImpl",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"groovy.lang.Closure"
|
||||
},
|
||||
{
|
||||
"name":"io.netty.bootstrap.ServerBootstrap$1"
|
||||
},
|
||||
{
|
||||
"name":"io.netty.bootstrap.ServerBootstrap$ServerBootstrapAcceptor",
|
||||
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }]
|
||||
},
|
||||
{
|
||||
"name":"io.netty.buffer.AbstractByteBufAllocator",
|
||||
"queryAllDeclaredMethods":true
|
||||
},
|
||||
{
|
||||
"name":"io.netty.buffer.AbstractReferenceCountedByteBuf",
|
||||
"fields":[{"name":"refCnt"}]
|
||||
},
|
||||
{
|
||||
"name":"io.netty.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.unpadded.MpscUnpaddedArrayQueueConsumerIndexField",
|
||||
"fields":[{"name":"consumerIndex"}]
|
||||
},
|
||||
{
|
||||
"name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerIndexField",
|
||||
"fields":[{"name":"producerIndex"}]
|
||||
},
|
||||
{
|
||||
"name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerLimitField",
|
||||
"fields":[{"name":"producerLimit"}]
|
||||
},
|
||||
{
|
||||
"name":"java.io.FilePermission"
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Object",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true
|
||||
},
|
||||
{
|
||||
"name":"java.lang.ProcessHandle",
|
||||
"methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"java.lang.RuntimePermission"
|
||||
},
|
||||
{
|
||||
"name":"java.lang.System",
|
||||
"methods":[{"name":"console","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Thread",
|
||||
"fields":[{"name":"threadLocalRandomProbe"}]
|
||||
},
|
||||
{
|
||||
"name":"java.net.NetPermission"
|
||||
},
|
||||
{
|
||||
"name":"java.net.SocketPermission"
|
||||
},
|
||||
{
|
||||
"name":"java.net.URLPermission",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"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.AllPermission"
|
||||
},
|
||||
{
|
||||
"name":"java.security.KeyStoreSpi"
|
||||
},
|
||||
{
|
||||
"name":"java.security.SecureRandomParameters"
|
||||
},
|
||||
{
|
||||
"name":"java.security.SecurityPermission"
|
||||
},
|
||||
{
|
||||
"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.PropertyPermission"
|
||||
},
|
||||
{
|
||||
"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":"java.util.zip.Adler32",
|
||||
"methods":[{"name":"update","parameterTypes":["java.nio.ByteBuffer"] }]
|
||||
},
|
||||
{
|
||||
"name":"java.util.zip.CRC32",
|
||||
"methods":[{"name":"update","parameterTypes":["java.nio.ByteBuffer"] }]
|
||||
},
|
||||
{
|
||||
"name":"javax.security.auth.x500.X500Principal",
|
||||
"fields":[{"name":"thisX500Name"}],
|
||||
"methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }]
|
||||
},
|
||||
{
|
||||
"name":"javax.smartcardio.CardPermission"
|
||||
},
|
||||
{
|
||||
"name":"jdk.internal.misc.Unsafe",
|
||||
"methods":[{"name":"getUnsafe","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"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":"net.woggioni.rbcs.client.RemoteBuildCacheClient$sendRequest$1$operationComplete$timeoutHandler$1",
|
||||
"methods":[{"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.CacheContentHandler",
|
||||
"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.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":"storeFence","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"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"] }]
|
||||
}
|
||||
]
|
48
rbcs-cli/native-image/resource-config.json
Normal file
48
rbcs-cli/native-image/resource-config.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"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/rbcs-default.xml\\E"
|
||||
}, {
|
||||
"pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E"
|
||||
}, {
|
||||
"pattern":"\\Q/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd\\E"
|
||||
}, {
|
||||
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
|
||||
}]},
|
||||
"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,182 @@
|
||||
package net.woggioni.rbcs.cli.graal
|
||||
|
||||
import net.woggioni.jwo.NullOutputStream
|
||||
import net.woggioni.rbcs.api.Configuration
|
||||
import net.woggioni.rbcs.api.Configuration.User
|
||||
import net.woggioni.rbcs.api.Role
|
||||
import net.woggioni.rbcs.cli.RemoteBuildCacheServerCli
|
||||
import net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.GetCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.PutCommand
|
||||
import net.woggioni.rbcs.common.HostAndPort
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||
import net.woggioni.rbcs.common.RBCS
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer
|
||||
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
|
||||
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
|
||||
import net.woggioni.rbcs.server.configuration.Parser
|
||||
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.zip.Deflater
|
||||
import net.woggioni.rbcs.client.Configuration as ClientConfiguration
|
||||
import net.woggioni.rbcs.client.impl.Parser as ClientConfigurationParser
|
||||
|
||||
object GraalNativeImageConfiguration {
|
||||
@JvmStatic
|
||||
fun main(vararg args : String) {
|
||||
|
||||
val serverURL = URI.create("file:conf/rbcs-client.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,
|
||||
chunkSize = 0x1000
|
||||
),
|
||||
FileSystemCacheConfiguration(
|
||||
Path.of(System.getProperty("java.io.tmpdir")).resolve("rbcs"),
|
||||
maxAge = Duration.ofSeconds(3600),
|
||||
digestAlgorithm = "MD5",
|
||||
compressionLevel = Deflater.DEFAULT_COMPRESSION,
|
||||
compressionEnabled = false,
|
||||
chunkSize = 0x1000
|
||||
),
|
||||
MemcacheCacheConfiguration(
|
||||
listOf(MemcacheCacheConfiguration.Server(
|
||||
HostAndPort("127.0.0.1", 11211),
|
||||
1000,
|
||||
4)
|
||||
),
|
||||
Duration.ofSeconds(60),
|
||||
"MD5",
|
||||
null,
|
||||
1,
|
||||
0x1000
|
||||
)
|
||||
)
|
||||
|
||||
for (cache in caches) {
|
||||
val serverConfiguration = Configuration(
|
||||
"127.0.0.1",
|
||||
serverPort,
|
||||
100,
|
||||
null,
|
||||
Configuration.EventExecutor(true),
|
||||
Configuration.Connection(
|
||||
Duration.ofSeconds(10),
|
||||
Duration.ofSeconds(15),
|
||||
Duration.ofSeconds(15),
|
||||
0x10000,
|
||||
),
|
||||
users.asSequence().map { it.name to it }.toMap(),
|
||||
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
|
||||
cache,
|
||||
Configuration.BasicAuthentication(),
|
||||
null,
|
||||
)
|
||||
|
||||
MemcacheCacheConfiguration(
|
||||
listOf(
|
||||
MemcacheCacheConfiguration.Server(
|
||||
HostAndPort("127.0.0.1", 11211),
|
||||
1000,
|
||||
4
|
||||
)
|
||||
),
|
||||
Duration.ofSeconds(60),
|
||||
"MD5",
|
||||
null,
|
||||
1,
|
||||
0x1000
|
||||
)
|
||||
|
||||
val serverHandle = RemoteBuildCacheServer(serverConfiguration).run()
|
||||
|
||||
|
||||
val clientProfile = ClientConfiguration.Profile(
|
||||
URI.create("http://127.0.0.1:$serverPort/"),
|
||||
null,
|
||||
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) {
|
||||
}
|
||||
}
|
||||
RemoteBuildCacheServerCli.main("--help")
|
||||
}
|
||||
}
|
17
rbcs-cli/src/main/java/module-info.java
Normal file
17
rbcs-cli/src/main/java/module-info.java
Normal file
@@ -0,0 +1,17 @@
|
||||
module net.woggioni.rbcs.cli {
|
||||
requires org.slf4j;
|
||||
requires net.woggioni.rbcs.server;
|
||||
requires info.picocli;
|
||||
requires net.woggioni.rbcs.common;
|
||||
requires net.woggioni.rbcs.client;
|
||||
requires kotlin.stdlib;
|
||||
requires net.woggioni.jwo;
|
||||
requires net.woggioni.rbcs.api;
|
||||
|
||||
exports net.woggioni.rbcs.cli.impl.converters to info.picocli;
|
||||
opens net.woggioni.rbcs.cli.impl.commands to info.picocli;
|
||||
opens net.woggioni.rbcs.cli.impl to info.picocli;
|
||||
opens net.woggioni.rbcs.cli to info.picocli, net.woggioni.rbcs.common;
|
||||
|
||||
exports net.woggioni.rbcs.cli;
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
package net.woggioni.rbcs.cli
|
||||
|
||||
import net.woggioni.jwo.Application
|
||||
import net.woggioni.rbcs.cli.impl.AbstractVersionProvider
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.ClientCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.GetCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.PasswordHashCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.PutCommand
|
||||
import net.woggioni.rbcs.cli.impl.commands.ServerCommand
|
||||
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Model.CommandSpec
|
||||
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "rbcs", versionProvider = RemoteBuildCacheServerCli.VersionProvider::class
|
||||
)
|
||||
class RemoteBuildCacheServerCli : RbcsCommand() {
|
||||
|
||||
class VersionProvider : AbstractVersionProvider()
|
||||
companion object {
|
||||
private fun setPropertyIfNotPresent(key: String, value: String) {
|
||||
System.getProperty(key) ?: System.setProperty(key, value)
|
||||
}
|
||||
@JvmStatic
|
||||
fun main(vararg args: String) {
|
||||
setPropertyIfNotPresent("logback.configurationFile", "net/woggioni/rbcs/cli/logback.xml")
|
||||
setPropertyIfNotPresent("io.netty.leakDetectionLevel", "DISABLED")
|
||||
val currentClassLoader = RemoteBuildCacheServerCli::class.java.classLoader
|
||||
Thread.currentThread().contextClassLoader = currentClassLoader
|
||||
if(currentClassLoader.javaClass.name == "net.woggioni.envelope.loader.ModuleClassLoader") {
|
||||
//We're running in an envelope jar and custom URL protocols won't work
|
||||
RbcsUrlStreamHandlerFactory.install()
|
||||
}
|
||||
val log = createLogger<RemoteBuildCacheServerCli>()
|
||||
val app = Application.builder("rbcs")
|
||||
.configurationDirectoryEnvVar("RBCS_CONFIGURATION_DIR")
|
||||
.configurationDirectoryPropertyKey("net.woggioni.rbcs.conf.dir")
|
||||
.build()
|
||||
val rbcsCli = RemoteBuildCacheServerCli()
|
||||
val commandLine = CommandLine(rbcsCli)
|
||||
commandLine.setExecutionExceptionHandler { ex, cl, parseResult ->
|
||||
log.error(ex.message, ex)
|
||||
CommandLine.ExitCode.SOFTWARE
|
||||
}
|
||||
commandLine.addSubcommand(ServerCommand(app))
|
||||
commandLine.addSubcommand(PasswordHashCommand())
|
||||
commandLine.addSubcommand(
|
||||
CommandLine(ClientCommand(app)).apply {
|
||||
addSubcommand(BenchmarkCommand())
|
||||
addSubcommand(PutCommand())
|
||||
addSubcommand(GetCommand())
|
||||
addSubcommand(HealthCheckCommand())
|
||||
})
|
||||
System.exit(commandLine.execute(*args))
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Option(names = ["-V", "--version"], versionHelp = true)
|
||||
var versionHelp = false
|
||||
private set
|
||||
|
||||
@CommandLine.Spec
|
||||
private lateinit var spec: CommandSpec
|
||||
|
||||
|
||||
override fun run() {
|
||||
spec.commandLine().usage(System.out);
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.woggioni.gbcs.cli.impl
|
||||
package net.woggioni.rbcs.cli.impl
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.util.jar.Attributes
|
@@ -1,11 +1,11 @@
|
||||
package net.woggioni.gbcs.cli.impl
|
||||
package net.woggioni.rbcs.cli.impl
|
||||
|
||||
import net.woggioni.jwo.Application
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
abstract class GbcsCommand : Runnable {
|
||||
abstract class RbcsCommand : Runnable {
|
||||
|
||||
@CommandLine.Option(names = ["-h", "--help"], usageHelp = true)
|
||||
var usageHelp = false
|
@@ -0,0 +1,186 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.jwo.LongMath
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.cli.impl.converters.ByteSizeConverter
|
||||
import net.woggioni.rbcs.client.Configuration
|
||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.common.error
|
||||
import net.woggioni.rbcs.common.info
|
||||
import picocli.CommandLine
|
||||
import java.security.SecureRandom
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.random.Random
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "benchmark",
|
||||
description = ["Run a load test against the server"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class BenchmarkCommand : RbcsCommand() {
|
||||
companion object {
|
||||
private val log = createLogger<BenchmarkCommand>()
|
||||
|
||||
fun execute(profile : Configuration.Profile,
|
||||
numberOfEntries : Int,
|
||||
entrySize : Int,
|
||||
useRandomValue : Boolean,
|
||||
) {
|
||||
val progressThreshold = LongMath.ceilDiv(numberOfEntries.toLong(), 20)
|
||||
RemoteBuildCacheClient(profile).use { client ->
|
||||
val entryGenerator = sequence {
|
||||
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
|
||||
while (true) {
|
||||
val key = JWO.bytesToHex(random.nextBytes(16))
|
||||
val value = if (useRandomValue) {
|
||||
random.nextBytes(entrySize)
|
||||
} else {
|
||||
val byteValue = random.nextInt().toByte()
|
||||
ByteArray(entrySize) { _ -> byteValue }
|
||||
}
|
||||
yield(key to value)
|
||||
}
|
||||
}
|
||||
|
||||
log.info {
|
||||
"Starting insertion"
|
||||
}
|
||||
val entries = let {
|
||||
val completionCounter = AtomicLong(0)
|
||||
val completionQueue = LinkedBlockingQueue<Pair<String, ByteArray>>(numberOfEntries)
|
||||
val start = Instant.now()
|
||||
val semaphore = Semaphore(profile.maxConnections * 5)
|
||||
val iterator = entryGenerator.take(numberOfEntries).iterator()
|
||||
while (completionCounter.get() < numberOfEntries) {
|
||||
if (iterator.hasNext()) {
|
||||
val entry = iterator.next()
|
||||
semaphore.acquire()
|
||||
val future =
|
||||
client.put(entry.first, entry.second, CacheValueMetadata(null, null)).thenApply { entry }
|
||||
future.whenComplete { result, ex ->
|
||||
if (ex != null) {
|
||||
log.error(ex.message, ex)
|
||||
} else {
|
||||
completionQueue.put(result)
|
||||
}
|
||||
semaphore.release()
|
||||
val completed = completionCounter.incrementAndGet()
|
||||
if (completed.mod(progressThreshold) == 0L) {
|
||||
log.debug {
|
||||
"Inserted $completed / $numberOfEntries"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
|
||||
}
|
||||
}
|
||||
|
||||
val inserted = completionQueue.toList()
|
||||
val end = Instant.now()
|
||||
log.info {
|
||||
val elapsed = Duration.between(start, end).toMillis()
|
||||
val opsPerSecond = String.format("%.2f", numberOfEntries.toDouble() / elapsed * 1000)
|
||||
"Insertion rate: $opsPerSecond ops/s"
|
||||
}
|
||||
inserted
|
||||
}
|
||||
log.info {
|
||||
"Inserted ${entries.size} entries"
|
||||
}
|
||||
log.info {
|
||||
"Starting retrieval"
|
||||
}
|
||||
if (entries.isNotEmpty()) {
|
||||
val completionCounter = AtomicLong(0)
|
||||
val semaphore = Semaphore(profile.maxConnections * 5)
|
||||
val start = Instant.now()
|
||||
val it = entries.iterator()
|
||||
while (completionCounter.get() < entries.size) {
|
||||
if (it.hasNext()) {
|
||||
val entry = it.next()
|
||||
semaphore.acquire()
|
||||
val future = client.get(entry.first).thenApply {
|
||||
if (it == null) {
|
||||
log.error {
|
||||
"Missing entry for key '${entry.first}'"
|
||||
}
|
||||
} else if (!entry.second.contentEquals(it)) {
|
||||
log.error {
|
||||
"Retrieved a value different from what was inserted for key '${entry.first}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
future.whenComplete { _, _ ->
|
||||
val completed = completionCounter.incrementAndGet()
|
||||
if (completed.mod(progressThreshold) == 0L) {
|
||||
log.debug {
|
||||
"Retrieved $completed / ${entries.size}"
|
||||
}
|
||||
}
|
||||
semaphore.release()
|
||||
}
|
||||
} else {
|
||||
Thread.sleep(Duration.of(500, ChronoUnit.MILLIS))
|
||||
}
|
||||
}
|
||||
val end = Instant.now()
|
||||
log.info {
|
||||
val elapsed = Duration.between(start, end).toMillis()
|
||||
val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000)
|
||||
"Retrieval rate: $opsPerSecond ops/s"
|
||||
}
|
||||
} else {
|
||||
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Spec
|
||||
private lateinit var spec: CommandLine.Model.CommandSpec
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-e", "--entries"],
|
||||
description = ["Total number of elements to be added to the cache"],
|
||||
paramLabel = "NUMBER_OF_ENTRIES"
|
||||
)
|
||||
private var numberOfEntries = 1000
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-s", "--size"],
|
||||
description = ["Size of a cache value in bytes"],
|
||||
paramLabel = "SIZE",
|
||||
converter = [ByteSizeConverter::class]
|
||||
)
|
||||
private var size = 0x1000
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-r", "--random"],
|
||||
description = ["Insert completely random byte values"]
|
||||
)
|
||||
private var randomValues = false
|
||||
|
||||
override fun run() {
|
||||
val clientCommand = spec.parent().userObject() as ClientCommand
|
||||
val profile = clientCommand.profileName.let { profileName ->
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
execute(
|
||||
profile,
|
||||
numberOfEntries,
|
||||
size,
|
||||
randomValues
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,38 +1,46 @@
|
||||
package net.woggioni.gbcs.cli.impl.commands
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.gbcs.cli.impl.GbcsCommand
|
||||
import net.woggioni.gbcs.client.GbcsClient
|
||||
import net.woggioni.jwo.Application
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.client.Configuration
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import picocli.CommandLine
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.nio.file.Path
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "client",
|
||||
description = ["GBCS client"],
|
||||
description = ["RBCS client"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class ClientCommand(app : Application) : GbcsCommand() {
|
||||
class ClientCommand(app : Application) : RbcsCommand() {
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-c", "--configuration"],
|
||||
description = ["Path to the client configuration file"],
|
||||
paramLabel = "CONFIGURATION_FILE"
|
||||
)
|
||||
private var configurationFile : Path = findConfigurationFile(app, "gbcs-client.xml")
|
||||
private var configurationFile : Path = findConfigurationFile(app, "rbcs-client.xml")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-p", "--profile"],
|
||||
description = ["Name of the client profile to be used"],
|
||||
paramLabel = "PROFILE",
|
||||
required = true
|
||||
required = false
|
||||
)
|
||||
var profileName : String? = null
|
||||
get() = field ?: throw IllegalArgumentException("A profile name must be specified using the '-p' command line parameter")
|
||||
|
||||
val configuration : GbcsClient.Configuration by lazy {
|
||||
GbcsClient.Configuration.parse(configurationFile)
|
||||
val configuration : Configuration by lazy {
|
||||
Configuration.parse(configurationFile)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
val log = createLogger<ClientCommand>()
|
||||
log.debug {
|
||||
"Using configuration file '$configurationFile'"
|
||||
}
|
||||
println("Available profiles:")
|
||||
configuration.profiles.forEach { (profileName, _) ->
|
||||
println(profileName)
|
@@ -0,0 +1,59 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.client.Configuration
|
||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import picocli.CommandLine
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "get",
|
||||
description = ["Fetch a value from the cache with the specified key"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class GetCommand : RbcsCommand() {
|
||||
companion object {
|
||||
private val log = createLogger<GetCommand>()
|
||||
|
||||
fun execute(profile : Configuration.Profile, key : String, outputStream: OutputStream) {
|
||||
RemoteBuildCacheClient(profile).use { client ->
|
||||
client.get(key).thenApply { value ->
|
||||
value?.let {
|
||||
outputStream.use {
|
||||
it.write(value)
|
||||
}
|
||||
} ?: throw NoSuchElementException("No value found for key $key")
|
||||
}.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Spec
|
||||
private lateinit var spec: CommandLine.Model.CommandSpec
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-k", "--key"],
|
||||
description = ["The key for the new value"],
|
||||
paramLabel = "KEY"
|
||||
)
|
||||
private var key : String = ""
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-v", "--value"],
|
||||
description = ["Path to a file where the retrieved value will be written (defaults to stdout)"],
|
||||
paramLabel = "VALUE_FILE",
|
||||
)
|
||||
private var output : Path? = null
|
||||
|
||||
override fun run() {
|
||||
val clientCommand = spec.parent().userObject() as ClientCommand
|
||||
val profile = clientCommand.profileName.let { profileName ->
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
execute(profile, key, (output?.let(Files::newOutputStream) ?: System.out))
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.client.Configuration
|
||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import picocli.CommandLine
|
||||
import java.security.SecureRandom
|
||||
import kotlin.random.Random
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "health",
|
||||
description = ["Check server health"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class HealthCheckCommand : RbcsCommand() {
|
||||
companion object{
|
||||
private val log = createLogger<HealthCheckCommand>()
|
||||
|
||||
fun execute(profile : Configuration.Profile) {
|
||||
RemoteBuildCacheClient(profile).use { client ->
|
||||
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
|
||||
val nonce = ByteArray(0xa0)
|
||||
random.nextBytes(nonce)
|
||||
client.healthCheck(nonce).thenApply { value ->
|
||||
if(value == null) {
|
||||
throw IllegalStateException("Empty response from server")
|
||||
}
|
||||
val offset = value.size - nonce.size
|
||||
for(i in 0 until nonce.size) {
|
||||
val a = nonce[i]
|
||||
val b = value[offset + i]
|
||||
if(a != b) {
|
||||
throw IllegalStateException("Server nonce does not match")
|
||||
}
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Spec
|
||||
private lateinit var spec: CommandLine.Model.CommandSpec
|
||||
|
||||
override fun run() {
|
||||
val clientCommand = spec.parent().userObject() as ClientCommand
|
||||
val profile = clientCommand.profileName.let { profileName ->
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
execute(profile)
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
package net.woggioni.gbcs.cli.impl.commands
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword
|
||||
import net.woggioni.gbcs.cli.impl.GbcsCommand
|
||||
import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter
|
||||
import net.woggioni.jwo.UncloseableOutputStream
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter
|
||||
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
|
||||
import picocli.CommandLine
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
@@ -12,10 +12,10 @@ import java.io.PrintWriter
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "password",
|
||||
description = ["Generate a password hash to add to GBCS configuration file"],
|
||||
description = ["Generate a password hash to add to RBCS configuration file"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class PasswordHashCommand : GbcsCommand() {
|
||||
class PasswordHashCommand : RbcsCommand() {
|
||||
@CommandLine.Option(
|
||||
names = ["-o", "--output-file"],
|
||||
description = ["Write the output to a file instead of stdout"],
|
@@ -0,0 +1,113 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.jwo.Hash
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.jwo.NullOutputStream
|
||||
import net.woggioni.rbcs.api.CacheValueMetadata
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.client.Configuration
|
||||
import net.woggioni.rbcs.client.RemoteBuildCacheClient
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import picocli.CommandLine
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "put",
|
||||
description = ["Add or replace a value to the cache with the specified key"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class PutCommand : RbcsCommand() {
|
||||
companion object {
|
||||
private val log = createLogger<PutCommand>()
|
||||
|
||||
fun execute(profile: Configuration.Profile,
|
||||
actualKey : String,
|
||||
inputStream: InputStream,
|
||||
mimeType : String?,
|
||||
contentDisposition: String?
|
||||
) {
|
||||
RemoteBuildCacheClient(profile).use { client ->
|
||||
inputStream.use {
|
||||
client.put(actualKey, it.readAllBytes(), CacheValueMetadata(contentDisposition, mimeType))
|
||||
}.get()
|
||||
println(profile.serverURI.resolve(actualKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@CommandLine.Spec
|
||||
private lateinit var spec: CommandLine.Model.CommandSpec
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-k", "--key"],
|
||||
description = ["The key for the new value, randomly generated if omitted"],
|
||||
paramLabel = "KEY"
|
||||
)
|
||||
private var key : String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-i", "--inline"],
|
||||
description = ["File is to be displayed in the browser"],
|
||||
paramLabel = "INLINE",
|
||||
)
|
||||
private var inline : Boolean = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-t", "--type"],
|
||||
description = ["File mime type"],
|
||||
paramLabel = "MIME_TYPE",
|
||||
)
|
||||
private var mimeType : String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-v", "--value"],
|
||||
description = ["Path to a file containing the value to be added (defaults to stdin)"],
|
||||
paramLabel = "VALUE_FILE",
|
||||
)
|
||||
private var value : Path? = null
|
||||
|
||||
override fun run() {
|
||||
val clientCommand = spec.parent().userObject() as ClientCommand
|
||||
val profile = clientCommand.profileName.let { profileName ->
|
||||
clientCommand.configuration.profiles[profileName]
|
||||
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
|
||||
}
|
||||
RemoteBuildCacheClient(profile).use { client ->
|
||||
val inputStream : InputStream
|
||||
val mimeType : String?
|
||||
val contentDisposition : String?
|
||||
val valuePath = value
|
||||
val actualKey : String?
|
||||
if(valuePath != null) {
|
||||
inputStream = Files.newInputStream(valuePath)
|
||||
mimeType = this.mimeType ?: Files.probeContentType(valuePath)
|
||||
contentDisposition = if(inline) {
|
||||
"inline"
|
||||
} else {
|
||||
"attachment; filename=\"${valuePath.fileName}\""
|
||||
}
|
||||
actualKey = key ?: let {
|
||||
val md = Hash.Algorithm.SHA512.newInputStream(Files.newInputStream(valuePath)).use {
|
||||
JWO.copy(it, NullOutputStream())
|
||||
it.messageDigest
|
||||
}
|
||||
UUID.nameUUIDFromBytes(md.digest()).toString()
|
||||
}
|
||||
} else {
|
||||
inputStream = System.`in`
|
||||
mimeType = this.mimeType
|
||||
contentDisposition = if(inline) {
|
||||
"inline"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
actualKey = key ?: UUID.randomUUID().toString()
|
||||
}
|
||||
execute(profile, actualKey, inputStream, mimeType, contentDisposition)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
package net.woggioni.rbcs.cli.impl.commands
|
||||
|
||||
import net.woggioni.jwo.Application
|
||||
import net.woggioni.jwo.JWO
|
||||
import net.woggioni.rbcs.cli.impl.RbcsCommand
|
||||
import net.woggioni.rbcs.cli.impl.converters.DurationConverter
|
||||
import net.woggioni.rbcs.common.createLogger
|
||||
import net.woggioni.rbcs.common.debug
|
||||
import net.woggioni.rbcs.common.info
|
||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer
|
||||
import net.woggioni.rbcs.server.RemoteBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
|
||||
import picocli.CommandLine
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "server",
|
||||
description = ["RBCS server"],
|
||||
showDefaultValues = true
|
||||
)
|
||||
class ServerCommand(app : Application) : RbcsCommand() {
|
||||
companion object {
|
||||
private val log = createLogger<ServerCommand>()
|
||||
}
|
||||
|
||||
private fun createDefaultConfigurationFile(configurationFile: Path) {
|
||||
log.info {
|
||||
"Creating default configuration file at '$configurationFile'"
|
||||
}
|
||||
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
|
||||
Files.newOutputStream(configurationFile).use { outputStream ->
|
||||
defaultConfigurationFileResource.openStream().use { inputStream ->
|
||||
JWO.copy(inputStream, outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-t", "--timeout"],
|
||||
description = ["Exit after the specified time"],
|
||||
paramLabel = "TIMEOUT",
|
||||
converter = [DurationConverter::class]
|
||||
)
|
||||
private var timeout: Duration? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-c", "--config-file"],
|
||||
description = ["Read the application configuration from this file"],
|
||||
paramLabel = "CONFIG_FILE"
|
||||
)
|
||||
private var configurationFile: Path = findConfigurationFile(app, "rbcs-server.xml")
|
||||
|
||||
override fun run() {
|
||||
if (!Files.exists(configurationFile)) {
|
||||
Files.createDirectories(configurationFile.parent)
|
||||
createDefaultConfigurationFile(configurationFile)
|
||||
}
|
||||
|
||||
log.debug {
|
||||
"Using configuration file '$configurationFile'"
|
||||
}
|
||||
val configuration = RemoteBuildCacheServer.loadConfiguration(configurationFile)
|
||||
log.debug {
|
||||
ByteArrayOutputStream().also {
|
||||
RemoteBuildCacheServer.dumpConfiguration(configuration, it)
|
||||
}.let {
|
||||
"Server configuration:\n${String(it.toByteArray())}"
|
||||
}
|
||||
}
|
||||
val server = RemoteBuildCacheServer(configuration)
|
||||
val handle = server.run()
|
||||
val shutdownHook = Thread.ofPlatform().unstarted {
|
||||
handle.sendShutdownSignal()
|
||||
try {
|
||||
handle.get(60, TimeUnit.SECONDS)
|
||||
} catch (ex : Throwable) {
|
||||
log.warn(ex.message, ex)
|
||||
}
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook)
|
||||
if(timeout != null) {
|
||||
Thread.sleep(timeout)
|
||||
handle.sendShutdownSignal()
|
||||
}
|
||||
handle.get()
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package net.woggioni.rbcs.cli.impl.converters
|
||||
|
||||
import picocli.CommandLine
|
||||
|
||||
|
||||
class ByteSizeConverter : CommandLine.ITypeConverter<Int> {
|
||||
override fun convert(value: String): Int {
|
||||
return Integer.decode(value)
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package net.woggioni.rbcs.cli.impl.converters
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.time.Duration
|
||||
|
||||
|
||||
class DurationConverter : CommandLine.ITypeConverter<Duration> {
|
||||
override fun convert(value: String): Duration {
|
||||
return Duration.parse(value)
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package net.woggioni.rbcs.cli.impl.converters
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
|
||||
class InputStreamConverter : CommandLine.ITypeConverter<InputStream> {
|
||||
override fun convert(value: String): InputStream {
|
||||
return Files.newInputStream(Paths.get(value))
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.woggioni.gbcs.cli.impl.converters
|
||||
package net.woggioni.rbcs.cli.impl.converters
|
||||
|
||||
import picocli.CommandLine
|
||||
import java.io.OutputStream
|
19
rbcs-client/build.gradle
Normal file
19
rbcs-client/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
alias catalog.plugins.kotlin.jvm
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':rbcs-api')
|
||||
implementation project(':rbcs-common')
|
||||
implementation catalog.slf4j.api
|
||||
implementation catalog.netty.buffer
|
||||
implementation catalog.netty.handler
|
||||
implementation catalog.netty.transport
|
||||
implementation catalog.netty.common
|
||||
implementation catalog.netty.codec.http
|
||||
|
||||
testRuntimeOnly catalog.logback.classic
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
module net.woggioni.gbcs.client {
|
||||
module net.woggioni.rbcs.client {
|
||||
requires io.netty.handler;
|
||||
requires io.netty.codec.http;
|
||||
requires io.netty.transport;
|
||||
@@ -6,11 +6,12 @@ module net.woggioni.gbcs.client {
|
||||
requires io.netty.common;
|
||||
requires io.netty.buffer;
|
||||
requires java.xml;
|
||||
requires net.woggioni.gbcs.base;
|
||||
requires net.woggioni.rbcs.common;
|
||||
requires net.woggioni.rbcs.api;
|
||||
requires io.netty.codec;
|
||||
requires org.slf4j;
|
||||
|
||||
exports net.woggioni.gbcs.client;
|
||||
exports net.woggioni.rbcs.client;
|
||||
|
||||
opens net.woggioni.gbcs.client.schema;
|
||||
opens net.woggioni.rbcs.client.schema;
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package net.woggioni.rbcs.client
|
||||
|
||||
import net.woggioni.rbcs.client.impl.Parser
|
||||
import net.woggioni.rbcs.common.Xml
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
|
||||
data class Configuration(
|
||||
val profiles: Map<String, Profile>
|
||||
) {
|
||||
sealed class Authentication {
|
||||
data class TlsClientAuthenticationCredentials(
|
||||
val key: PrivateKey,
|
||||
val certificateChain: Array<X509Certificate>
|
||||
) : Authentication()
|
||||
|
||||
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
|
||||
}
|
||||
|
||||
class TrustStore (
|
||||
var file: Path?,
|
||||
var password: String?,
|
||||
var checkCertificateStatus: Boolean = false,
|
||||
var verifyServerCertificate: Boolean = true,
|
||||
)
|
||||
|
||||
class RetryPolicy(
|
||||
val maxAttempts: Int,
|
||||
val initialDelayMillis: Long,
|
||||
val exp: Double
|
||||
)
|
||||
|
||||
class Connection(
|
||||
val readIdleTimeout: Duration,
|
||||
val writeIdleTimeout: Duration,
|
||||
val idleTimeout: Duration,
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package net.woggioni.gbcs.client
|
||||
package net.woggioni.rbcs.client
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
|
@@ -0,0 +1,419 @@
|
||||
package net.woggioni.rbcs.client
|
||||
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.Channel
|
||||
import io.netty.channel.ChannelHandler
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
import io.netty.channel.ChannelOption
|
||||
import io.netty.channel.ChannelPipeline
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.channel.pool.AbstractChannelPoolHandler
|
||||
import io.netty.channel.pool.ChannelPool
|
||||
import io.netty.channel.pool.FixedChannelPool
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import io.netty.handler.codec.DecoderException
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest
|
||||
import io.netty.handler.codec.http.FullHttpRequest
|
||||
import io.netty.handler.codec.http.FullHttpResponse
|
||||
import io.netty.handler.codec.http.HttpClientCodec
|
||||
import io.netty.handler.codec.http.HttpContentDecompressor
|
||||
import io.netty.handler.codec.http.HttpHeaderNames
|
||||
import io.netty.handler.codec.http.HttpHeaderValues
|
||||
import io.netty.handler.codec.http.HttpMethod
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import io.netty.handler.codec.http.HttpVersion
|
||||
import io.netty.handler.ssl.SslContext
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import io.netty.handler.stream.ChunkedWriteHandler
|
||||
import io.netty.handler.timeout.IdleState
|
||||
import io.netty.handler.timeout.IdleStateEvent
|
||||
import io.netty.handler.timeout.IdleStateHandler
|
||||
import io.netty.util.concurrent.Future
|
||||
import io.netty.util.concurrent.GenericFutureListener
|
||||
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
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Base64
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import kotlin.random.Random
|
||||
import io.netty.util.concurrent.Future as NettyFuture
|
||||
|
||||
class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
|
||||
companion object{
|
||||
private val log = createLogger<RemoteBuildCacheClient>()
|
||||
}
|
||||
|
||||
private val group: NioEventLoopGroup
|
||||
private val sslContext: SslContext
|
||||
private val pool: ChannelPool
|
||||
|
||||
init {
|
||||
group = NioEventLoopGroup()
|
||||
sslContext = SslContextBuilder.forClient().also { builder ->
|
||||
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
|
||||
builder.apply {
|
||||
keyManager(
|
||||
tlsClientAuthenticationCredentials.key,
|
||||
*tlsClientAuthenticationCredentials.certificateChain
|
||||
)
|
||||
profile.tlsTruststore?.let { trustStore ->
|
||||
if(!trustStore.verifyServerCertificate) {
|
||||
trustManager(object : X509TrustManager {
|
||||
override fun checkClientTrusted(certChain: Array<out X509Certificate>, p1: String?) {
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(certChain: Array<out X509Certificate>, p1: String?) {
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers() = null
|
||||
})
|
||||
} else {
|
||||
trustStore.file?.let {
|
||||
val ts = loadKeystore(it, trustStore.password)
|
||||
val trustManagerFactory: TrustManagerFactory =
|
||||
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(ts)
|
||||
trustManager(trustManagerFactory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
val (scheme, host, port) = profile.serverURI.run {
|
||||
Triple(
|
||||
if (scheme == null) "http" else profile.serverURI.scheme,
|
||||
host,
|
||||
port.takeIf { it > 0 } ?: if ("https" == scheme.lowercase()) 443 else 80
|
||||
)
|
||||
}
|
||||
|
||||
val bootstrap = Bootstrap().apply {
|
||||
group(group)
|
||||
channel(NioSocketChannel::class.java)
|
||||
option(ChannelOption.TCP_NODELAY, true)
|
||||
option(ChannelOption.SO_KEEPALIVE, true)
|
||||
remoteAddress(InetSocketAddress(host, port))
|
||||
profile.connectionTimeout?.let {
|
||||
option(ChannelOption.CONNECT_TIMEOUT_MILLIS, it.toMillis().toInt())
|
||||
}
|
||||
}
|
||||
val channelPoolHandler = object : AbstractChannelPoolHandler() {
|
||||
|
||||
@Volatile
|
||||
private var connectionCount = AtomicInteger()
|
||||
|
||||
@Volatile
|
||||
private var leaseCount = AtomicInteger()
|
||||
|
||||
override fun channelReleased(ch: Channel) {
|
||||
val activeLeases = leaseCount.decrementAndGet()
|
||||
log.trace {
|
||||
"Released channel ${ch.id().asShortText()}, number of active leases: $activeLeases"
|
||||
}
|
||||
}
|
||||
|
||||
override fun channelAcquired(ch: Channel) {
|
||||
val activeLeases = leaseCount.getAndIncrement()
|
||||
log.trace {
|
||||
"Acquired channel ${ch.id().asShortText()}, number of active leases: $activeLeases"
|
||||
}
|
||||
}
|
||||
|
||||
override fun channelCreated(ch: Channel) {
|
||||
val connectionId = connectionCount.incrementAndGet()
|
||||
log.debug {
|
||||
"Created connection ${ch.id().asShortText()}, total number of active connections: $connectionId"
|
||||
}
|
||||
ch.closeFuture().addListener {
|
||||
val activeConnections = connectionCount.decrementAndGet()
|
||||
log.debug {
|
||||
"Closed connection ${
|
||||
ch.id().asShortText()
|
||||
}, total number of active connections: $activeConnections"
|
||||
}
|
||||
}
|
||||
val pipeline: ChannelPipeline = ch.pipeline()
|
||||
|
||||
profile.connection?.also { conn ->
|
||||
val readIdleTimeout = conn.readIdleTimeout.toMillis()
|
||||
val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
|
||||
val idleTimeout = conn.idleTimeout.toMillis()
|
||||
if (readIdleTimeout > 0 || writeIdleTimeout > 0 || idleTimeout > 0) {
|
||||
pipeline.addLast(
|
||||
IdleStateHandler(
|
||||
true,
|
||||
readIdleTimeout,
|
||||
writeIdleTimeout,
|
||||
idleTimeout,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add SSL handler if needed
|
||||
if ("https".equals(scheme, ignoreCase = true)) {
|
||||
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc(), host, port))
|
||||
}
|
||||
|
||||
// HTTP handlers
|
||||
pipeline.addLast("codec", HttpClientCodec())
|
||||
if(profile.compressionEnabled) {
|
||||
pipeline.addLast("decompressor", HttpContentDecompressor())
|
||||
}
|
||||
pipeline.addLast("aggregator", HttpObjectAggregator(134217728))
|
||||
pipeline.addLast("chunked", ChunkedWriteHandler())
|
||||
}
|
||||
}
|
||||
pool = FixedChannelPool(bootstrap, channelPoolHandler, profile.maxConnections)
|
||||
}
|
||||
|
||||
private fun executeWithRetry(operation: () -> CompletableFuture<FullHttpResponse>): CompletableFuture<FullHttpResponse> {
|
||||
val retryPolicy = profile.retryPolicy
|
||||
return if (retryPolicy != null) {
|
||||
val outcomeHandler = OutcomeHandler<FullHttpResponse> { outcome ->
|
||||
when (outcome) {
|
||||
is OperationOutcome.Success -> {
|
||||
val response = outcome.result
|
||||
val status = response.status()
|
||||
when (status) {
|
||||
HttpResponseStatus.TOO_MANY_REQUESTS -> {
|
||||
val retryAfter = response.headers()[HttpHeaderNames.RETRY_AFTER]?.let { headerValue ->
|
||||
try {
|
||||
headerValue.toLong() * 1000
|
||||
} catch (nfe: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
OutcomeHandlerResult.Retry(retryAfter)
|
||||
}
|
||||
|
||||
HttpResponseStatus.INTERNAL_SERVER_ERROR, HttpResponseStatus.SERVICE_UNAVAILABLE ->
|
||||
OutcomeHandlerResult.Retry()
|
||||
|
||||
else -> OutcomeHandlerResult.DoNotRetry()
|
||||
}
|
||||
}
|
||||
|
||||
is OperationOutcome.Failure -> {
|
||||
OutcomeHandlerResult.Retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
executeWithRetry(
|
||||
group,
|
||||
retryPolicy.maxAttempts,
|
||||
retryPolicy.initialDelayMillis.toDouble(),
|
||||
retryPolicy.exp,
|
||||
outcomeHandler,
|
||||
Random.Default,
|
||||
operation
|
||||
)
|
||||
} else {
|
||||
operation()
|
||||
}
|
||||
}
|
||||
|
||||
fun healthCheck(nonce: ByteArray): CompletableFuture<ByteArray?> {
|
||||
return executeWithRetry {
|
||||
sendRequest(profile.serverURI, HttpMethod.TRACE, nonce)
|
||||
}.thenApply {
|
||||
val status = it.status()
|
||||
if (it.status() != HttpResponseStatus.OK) {
|
||||
throw HttpException(status)
|
||||
} else {
|
||||
it.content()
|
||||
}
|
||||
}.thenApply { maybeByteBuf ->
|
||||
maybeByteBuf?.let {
|
||||
val result = ByteArray(it.readableBytes())
|
||||
it.getBytes(0, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun get(key: String): CompletableFuture<ByteArray?> {
|
||||
return executeWithRetry {
|
||||
sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)
|
||||
}.thenApply {
|
||||
val status = it.status()
|
||||
if (it.status() == HttpResponseStatus.NOT_FOUND) {
|
||||
null
|
||||
} else if (it.status() != HttpResponseStatus.OK) {
|
||||
throw HttpException(status)
|
||||
} else {
|
||||
it.content()
|
||||
}
|
||||
}.thenApply { maybeByteBuf ->
|
||||
maybeByteBuf?.let {
|
||||
val result = ByteArray(it.readableBytes())
|
||||
it.getBytes(0, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun put(key: String, content: ByteArray, metadata: CacheValueMetadata): CompletableFuture<Unit> {
|
||||
return executeWithRetry {
|
||||
val extraHeaders = sequenceOf(
|
||||
metadata.mimeType?.let { HttpHeaderNames.CONTENT_TYPE to it },
|
||||
metadata.contentDisposition?.let { HttpHeaderNames.CONTENT_DISPOSITION to it }
|
||||
).filterNotNull()
|
||||
sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content, extraHeaders.asIterable())
|
||||
}.thenApply {
|
||||
val status = it.status()
|
||||
if (it.status() != HttpResponseStatus.CREATED && it.status() != HttpResponseStatus.OK) {
|
||||
throw HttpException(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendRequest(
|
||||
uri: URI,
|
||||
method: HttpMethod,
|
||||
body: ByteArray?,
|
||||
extraHeaders: Iterable<Pair<CharSequence, CharSequence>>? = null
|
||||
): CompletableFuture<FullHttpResponse> {
|
||||
val responseFuture = CompletableFuture<FullHttpResponse>()
|
||||
// Custom handler for processing responses
|
||||
|
||||
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
|
||||
private val handlers = mutableListOf<ChannelHandler>()
|
||||
|
||||
fun cleanup(channel: Channel, pipeline: ChannelPipeline) {
|
||||
handlers.forEach(pipeline::remove)
|
||||
pool.release(channel)
|
||||
}
|
||||
|
||||
override fun operationComplete(channelFuture: Future<Channel>) {
|
||||
if (channelFuture.isSuccess) {
|
||||
val channel = channelFuture.now
|
||||
val pipeline = channel.pipeline()
|
||||
val timeoutHandler = object : ChannelInboundHandlerAdapter() {
|
||||
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||
if (evt is IdleStateEvent) {
|
||||
val te = when (evt.state()) {
|
||||
IdleState.READER_IDLE -> TimeoutException(
|
||||
"Read timeout",
|
||||
)
|
||||
|
||||
IdleState.WRITER_IDLE -> TimeoutException("Write timeout")
|
||||
|
||||
IdleState.ALL_IDLE -> TimeoutException("Idle timeout")
|
||||
null -> throw IllegalStateException("This should never happen")
|
||||
}
|
||||
responseFuture.completeExceptionally(te)
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
val closeListener = GenericFutureListener<Future<Void>> {
|
||||
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
||||
pool.release(channel)
|
||||
}
|
||||
|
||||
val responseHandler = object : SimpleChannelInboundHandler<FullHttpResponse>() {
|
||||
override fun channelRead0(
|
||||
ctx: ChannelHandlerContext,
|
||||
response: FullHttpResponse
|
||||
) {
|
||||
channel.closeFuture().removeListener(closeListener)
|
||||
cleanup(channel, pipeline)
|
||||
responseFuture.complete(response)
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
ctx.newPromise()
|
||||
val ex = when (cause) {
|
||||
is DecoderException -> cause.cause
|
||||
else -> cause
|
||||
}
|
||||
responseFuture.completeExceptionally(ex)
|
||||
ctx.close()
|
||||
}
|
||||
|
||||
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||
pool.release(channel)
|
||||
responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
|
||||
super.channelInactive(ctx)
|
||||
}
|
||||
}
|
||||
for (handler in arrayOf(timeoutHandler, responseHandler)) {
|
||||
handlers.add(handler)
|
||||
}
|
||||
pipeline.addLast(timeoutHandler, responseHandler)
|
||||
channel.closeFuture().addListener(closeListener)
|
||||
|
||||
|
||||
// Prepare the HTTP request
|
||||
val request: FullHttpRequest = let {
|
||||
val content: ByteBuf? = body?.takeIf(ByteArray::isNotEmpty)?.let(Unpooled::wrappedBuffer)
|
||||
DefaultFullHttpRequest(
|
||||
HttpVersion.HTTP_1_1,
|
||||
method,
|
||||
uri.rawPath,
|
||||
content ?: Unpooled.buffer(0)
|
||||
).apply {
|
||||
headers().apply {
|
||||
if (content != null) {
|
||||
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
|
||||
}
|
||||
set(HttpHeaderNames.HOST, profile.serverURI.host)
|
||||
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
|
||||
if(profile.compressionEnabled) {
|
||||
set(
|
||||
HttpHeaderNames.ACCEPT_ENCODING,
|
||||
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
|
||||
)
|
||||
}
|
||||
extraHeaders?.forEach { (k, v) ->
|
||||
add(k, v)
|
||||
}
|
||||
// Add basic auth if configured
|
||||
(profile.authentication as? Configuration.Authentication.BasicAuthenticationCredentials)?.let { credentials ->
|
||||
val auth = "${credentials.username}:${credentials.password}"
|
||||
val encodedAuth = Base64.getEncoder().encodeToString(auth.toByteArray())
|
||||
set(HttpHeaderNames.AUTHORIZATION, "Basic $encodedAuth")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set headers
|
||||
// Send the request
|
||||
channel.writeAndFlush(request)
|
||||
} else {
|
||||
responseFuture.completeExceptionally(channelFuture.cause())
|
||||
}
|
||||
}
|
||||
})
|
||||
return responseFuture
|
||||
}
|
||||
|
||||
fun shutDown(): NettyFuture<*> {
|
||||
return group.shutdownGracefully()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
shutDown().sync()
|
||||
}
|
||||
}
|
@@ -0,0 +1,143 @@
|
||||
package net.woggioni.rbcs.client.impl
|
||||
|
||||
import net.woggioni.rbcs.api.exception.ConfigurationException
|
||||
import net.woggioni.rbcs.client.Configuration
|
||||
import net.woggioni.rbcs.common.Xml.Companion.asIterable
|
||||
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
||||
import org.w3c.dom.Document
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
object Parser {
|
||||
|
||||
fun parse(document: Document): Configuration {
|
||||
val root = document.documentElement
|
||||
val profiles = mutableMapOf<String, Configuration.Profile>()
|
||||
|
||||
for (child in root.asIterable()) {
|
||||
val tagName = child.localName
|
||||
when (tagName) {
|
||||
"profile" -> {
|
||||
val name =
|
||||
child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
|
||||
val uri = child.renderAttribute("base-url")?.let(::URI)
|
||||
?: throw ConfigurationException("base-url attribute is required")
|
||||
var authentication: Configuration.Authentication? = null
|
||||
var retryPolicy: Configuration.RetryPolicy? = null
|
||||
var connection : Configuration.Connection? = null
|
||||
var trustStore : Configuration.TrustStore? = null
|
||||
for (gchild in child.asIterable()) {
|
||||
when (gchild.localName) {
|
||||
"tls-client-auth" -> {
|
||||
val keyStoreFile = gchild.renderAttribute("key-store-file")
|
||||
val keyStorePassword =
|
||||
gchild.renderAttribute("key-store-password")
|
||||
val keyAlias = gchild.renderAttribute("key-alias")
|
||||
val keyPassword = gchild.renderAttribute("key-password")
|
||||
|
||||
val keystore = KeyStore.getInstance("PKCS12").apply {
|
||||
Files.newInputStream(Path.of(keyStoreFile)).use {
|
||||
load(it, keyStorePassword?.toCharArray())
|
||||
}
|
||||
}
|
||||
val key = keystore.getKey(keyAlias, keyPassword?.toCharArray()) as PrivateKey
|
||||
val certChain = keystore.getCertificateChain(keyAlias).asSequence()
|
||||
.map { it as X509Certificate }
|
||||
.toList()
|
||||
.toTypedArray()
|
||||
authentication =
|
||||
Configuration.Authentication.TlsClientAuthenticationCredentials(
|
||||
key,
|
||||
certChain
|
||||
)
|
||||
}
|
||||
|
||||
"basic-auth" -> {
|
||||
val username = gchild.renderAttribute("user")
|
||||
?: throw ConfigurationException("username attribute is required")
|
||||
val password = gchild.renderAttribute("password")
|
||||
?: throw ConfigurationException("password attribute is required")
|
||||
authentication =
|
||||
Configuration.Authentication.BasicAuthenticationCredentials(
|
||||
username,
|
||||
password
|
||||
)
|
||||
}
|
||||
|
||||
"retry-policy" -> {
|
||||
val maxAttempts =
|
||||
gchild.renderAttribute("max-attempts")
|
||||
?.let(String::toInt)
|
||||
?: throw ConfigurationException("max-attempts attribute is required")
|
||||
val initialDelay =
|
||||
gchild.renderAttribute("initial-delay")
|
||||
?.let(Duration::parse)
|
||||
?: Duration.ofSeconds(1)
|
||||
val exp =
|
||||
gchild.renderAttribute("exp")
|
||||
?.let(String::toDouble)
|
||||
?: 2.0f
|
||||
retryPolicy = Configuration.RetryPolicy(
|
||||
maxAttempts,
|
||||
initialDelay.toMillis(),
|
||||
exp.toDouble()
|
||||
)
|
||||
}
|
||||
|
||||
"connection" -> {
|
||||
val idleTimeout = gchild.renderAttribute("idle-timeout")
|
||||
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
|
||||
val readIdleTimeout = gchild.renderAttribute("read-idle-timeout")
|
||||
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
||||
val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout")
|
||||
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
||||
connection = Configuration.Connection(
|
||||
readIdleTimeout,
|
||||
writeIdleTimeout,
|
||||
idleTimeout,
|
||||
)
|
||||
}
|
||||
|
||||
"tls-trust-store" -> {
|
||||
val file = gchild.renderAttribute("file")
|
||||
?.let(Path::of)
|
||||
val password = gchild.renderAttribute("password")
|
||||
val checkCertificateStatus = gchild.renderAttribute("check-certificate-status")
|
||||
?.let(String::toBoolean) ?: false
|
||||
val verifyServerCertificate = gchild.renderAttribute("verify-server-certificate")
|
||||
?.let(String::toBoolean) ?: true
|
||||
trustStore = Configuration.TrustStore(file, password, checkCertificateStatus, verifyServerCertificate)
|
||||
}
|
||||
}
|
||||
}
|
||||
val maxConnections = child.renderAttribute("max-connections")
|
||||
?.let(String::toInt)
|
||||
?: 50
|
||||
val connectionTimeout = child.renderAttribute("connection-timeout")
|
||||
?.let(Duration::parse)
|
||||
val compressionEnabled = child.renderAttribute("enable-compression")
|
||||
?.let(String::toBoolean)
|
||||
?: true
|
||||
|
||||
profiles[name] = Configuration.Profile(
|
||||
uri,
|
||||
connection,
|
||||
authentication,
|
||||
connectionTimeout,
|
||||
maxConnections,
|
||||
compressionEnabled,
|
||||
retryPolicy,
|
||||
trustStore
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Configuration(profiles)
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
package net.woggioni.rbcs.client
|
||||
|
||||
import io.netty.util.concurrent.EventExecutorGroup
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.pow
|
||||
import kotlin.random.Random
|
||||
|
||||
sealed class OperationOutcome<T> {
|
||||
class Success<T>(val result: T) : OperationOutcome<T>()
|
||||
class Failure<T>(val ex: Throwable) : OperationOutcome<T>()
|
||||
}
|
||||
|
||||
sealed class OutcomeHandlerResult {
|
||||
class Retry(val suggestedDelayMillis: Long? = null) : OutcomeHandlerResult()
|
||||
class DoNotRetry : OutcomeHandlerResult()
|
||||
}
|
||||
|
||||
fun interface OutcomeHandler<T> {
|
||||
fun shouldRetry(result: OperationOutcome<T>): OutcomeHandlerResult
|
||||
}
|
||||
|
||||
fun <T> executeWithRetry(
|
||||
eventExecutorGroup: EventExecutorGroup,
|
||||
maxAttempts: Int,
|
||||
initialDelay: Double,
|
||||
exp: Double,
|
||||
outcomeHandler: OutcomeHandler<T>,
|
||||
randomizer : Random?,
|
||||
cb: () -> CompletableFuture<T>
|
||||
): CompletableFuture<T> {
|
||||
|
||||
val finalResult = cb()
|
||||
var future = finalResult
|
||||
var shortCircuit = false
|
||||
for (i in 1 until maxAttempts) {
|
||||
future = future.handle { result, ex ->
|
||||
val operationOutcome = if (ex == null) {
|
||||
OperationOutcome.Success(result)
|
||||
} else {
|
||||
OperationOutcome.Failure(ex.cause ?: ex)
|
||||
}
|
||||
if (shortCircuit) {
|
||||
when(operationOutcome) {
|
||||
is OperationOutcome.Failure -> throw operationOutcome.ex
|
||||
is OperationOutcome.Success -> CompletableFuture.completedFuture(operationOutcome.result)
|
||||
}
|
||||
} else {
|
||||
when(val outcomeHandlerResult = outcomeHandler.shouldRetry(operationOutcome)) {
|
||||
is OutcomeHandlerResult.Retry -> {
|
||||
val res = CompletableFuture<T>()
|
||||
val delay = run {
|
||||
val scheduledDelay = (initialDelay * exp.pow(i.toDouble()) * (1.0 + (randomizer?.nextDouble(-0.5, 0.5) ?: 0.0))).toLong()
|
||||
outcomeHandlerResult.suggestedDelayMillis?.coerceAtMost(scheduledDelay) ?: scheduledDelay
|
||||
}
|
||||
eventExecutorGroup.schedule({
|
||||
cb().handle { result, ex ->
|
||||
if (ex == null) {
|
||||
res.complete(result)
|
||||
} else {
|
||||
res.completeExceptionally(ex)
|
||||
}
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS)
|
||||
res
|
||||
}
|
||||
is OutcomeHandlerResult.DoNotRetry -> {
|
||||
shortCircuit = true
|
||||
when(operationOutcome) {
|
||||
is OperationOutcome.Failure -> throw operationOutcome.ex
|
||||
is OperationOutcome.Success -> CompletableFuture.completedFuture(operationOutcome.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.thenCompose { it }
|
||||
}
|
||||
return future
|
||||
}
|
@@ -0,0 +1,253 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<xs:schema targetNamespace="urn:net.woggioni.rbcs.client"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:rbcs-client="urn:net.woggioni.rbcs.client"
|
||||
elementFormDefault="unqualified"
|
||||
>
|
||||
<xs:element name="profiles" type="rbcs-client:profilesType"/>
|
||||
|
||||
<xs:complexType name="profilesType">
|
||||
<xs:sequence minOccurs="0">
|
||||
<xs:element name="profile" type="rbcs-client:profileType" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="profileType">
|
||||
<xs:sequence>
|
||||
<xs:choice>
|
||||
<xs:element name="no-auth" type="rbcs-client:noAuthType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Disable authentication.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="basic-auth" type="rbcs-client:basicAuthType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Enable HTTP basic authentication.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Enable TLS certificate authentication.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
<xs:element name="connection" type="rbcs-client:connectionType" minOccurs="0" >
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Set inactivity timeouts for connections to this server,
|
||||
if not present, connections are only closed on network errors.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="retry-policy" type="rbcs-client:retryType" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Set a retry policy for this server, if not present requests won't be retried
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="tls-trust-store" type="rbcs-client:trustStoreType" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
If set, specify an alternative truststore to validate the server certificate.
|
||||
If not present the system truststore is used.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:token" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Name of this server profile, to be referred to from rbcs-cli with the '-p' parameter
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="base-url" type="xs:anyURI" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
RBCs server URL
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Maximum number of concurrent TCP connection to open with this server
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="connection-timeout" type="xs:duration">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Enable HTTP compression when communicating to this server
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="enable-compression" type="xs:boolean" default="true">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Enable HTTP compression when communicating to this server
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="connectionType">
|
||||
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
The client will close the connection with the server
|
||||
when neither a read nor a write was performed for the specified period of time.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
The client will close the connection with the server
|
||||
when no read was performed for the specified period of time.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
The client will close the connection with the server
|
||||
when no write was performed for the specified period of time.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="noAuthType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Add this tag to not use any type of authentication when talking to the RBCS server
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="basicAuthType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Add this tag to enable HTTP basic authentication for the communication to this server,
|
||||
mind that HTTP basic authentication sends credentials directly over the network, so make sure
|
||||
your communication is protected by TLS (i.e. your server's URL starts with "https")
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:attribute name="user" type="xs:token" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Username for HTTP basic authentication
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="password" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Password used for HTTP basic authentication
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="tlsClientAuthType">
|
||||
<xs:attribute name="key-store-file" type="xs:anyURI" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
System path to the keystore file
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="key-store-password" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Password to open they keystore file
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="key-alias" type="xs:token" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Alias of the keystore entry containing the private key
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="key-password" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Private key entry's encryption password
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="retryType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Retry policy to use in case of failures, based on exponential backoff
|
||||
https://en.wikipedia.org/wiki/Exponential_backoff
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
<xs:attribute name="max-attempts" type="xs:positiveInteger" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Maximum number of attempts, after which the call will result in an error,
|
||||
throwing an exception related to the last received failure
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Delay to apply before retrying after the first failed call
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="exp" type="xs:double" default="2.0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Exponent to apply to compute the next delay
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="trustStoreType">
|
||||
<xs:attribute name="file" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Path to the truststore file
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="password" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Truststore file password
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="check-certificate-status" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Whether or not check the server certificate validity using CRL/OCSP
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="verify-server-certificate" type="xs:boolean" use="optional" default="true">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
If false, the client will blindly trust the certificate provided by the server
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
@@ -0,0 +1,148 @@
|
||||
package net.woggioni.rbcs.client
|
||||
|
||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||
import io.netty.util.concurrent.EventExecutorGroup
|
||||
import net.woggioni.rbcs.common.contextLogger
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import org.junit.jupiter.params.provider.ArgumentsProvider
|
||||
import org.junit.jupiter.params.provider.ArgumentsSource
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.stream.Stream
|
||||
import kotlin.random.Random
|
||||
|
||||
class RetryTest {
|
||||
|
||||
data class TestArgs(
|
||||
val seed: Int,
|
||||
val maxAttempt: Int,
|
||||
val initialDelay: Double,
|
||||
val exp: Double,
|
||||
)
|
||||
|
||||
class TestArguments : ArgumentsProvider {
|
||||
override fun provideArguments(context: ExtensionContext): Stream<out Arguments> {
|
||||
return Stream.of(
|
||||
TestArgs(
|
||||
seed = 101325,
|
||||
maxAttempt = 5,
|
||||
initialDelay = 50.0,
|
||||
exp = 2.0,
|
||||
),
|
||||
TestArgs(
|
||||
seed = 101325,
|
||||
maxAttempt = 20,
|
||||
initialDelay = 100.0,
|
||||
exp = 1.1,
|
||||
),
|
||||
TestArgs(
|
||||
seed = 123487,
|
||||
maxAttempt = 20,
|
||||
initialDelay = 100.0,
|
||||
exp = 2.0,
|
||||
),
|
||||
TestArgs(
|
||||
seed = 20082024,
|
||||
maxAttempt = 10,
|
||||
initialDelay = 100.0,
|
||||
exp = 2.0,
|
||||
)
|
||||
).map {
|
||||
object: Arguments {
|
||||
override fun get() = arrayOf(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ArgumentsSource(TestArguments::class)
|
||||
@ParameterizedTest
|
||||
fun test(testArgs: TestArgs) {
|
||||
val log = contextLogger()
|
||||
log.debug("Start")
|
||||
val executor: EventExecutorGroup = DefaultEventExecutorGroup(1)
|
||||
val attempts = mutableListOf<Pair<Long, OperationOutcome<Int>>>()
|
||||
val outcomeHandler = OutcomeHandler<Int> { outcome ->
|
||||
when(outcome) {
|
||||
is OperationOutcome.Success -> {
|
||||
if(outcome.result % 10 == 0) {
|
||||
OutcomeHandlerResult.DoNotRetry()
|
||||
} else {
|
||||
OutcomeHandlerResult.Retry(null)
|
||||
}
|
||||
}
|
||||
is OperationOutcome.Failure -> {
|
||||
when(outcome.ex) {
|
||||
is IllegalStateException -> {
|
||||
log.debug(outcome.ex.message, outcome.ex)
|
||||
OutcomeHandlerResult.Retry(null)
|
||||
}
|
||||
else -> {
|
||||
OutcomeHandlerResult.DoNotRetry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val random = Random(testArgs.seed)
|
||||
|
||||
val future =
|
||||
executeWithRetry(executor, testArgs.maxAttempt, testArgs.initialDelay, testArgs.exp, outcomeHandler, null) {
|
||||
val now = System.nanoTime()
|
||||
val result = CompletableFuture<Int>()
|
||||
executor.submit {
|
||||
val n = random.nextInt(0, Integer.MAX_VALUE)
|
||||
log.debug("Got new number: {}", n)
|
||||
if(n % 3 == 0) {
|
||||
val ex = IllegalStateException("Value $n can be divided by 3")
|
||||
result.completeExceptionally(ex)
|
||||
attempts += now to OperationOutcome.Failure(ex)
|
||||
} else if(n % 7 == 0) {
|
||||
val ex = RuntimeException("Value $n can be divided by 7")
|
||||
result.completeExceptionally(ex)
|
||||
attempts += now to OperationOutcome.Failure(ex)
|
||||
} else {
|
||||
result.complete(n)
|
||||
attempts += now to OperationOutcome.Success(n)
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
Assertions.assertTrue(attempts.size <= testArgs.maxAttempt)
|
||||
val result = future.handle { res, ex ->
|
||||
if(ex != null) {
|
||||
val err = ex.cause ?: ex
|
||||
log.debug(err.message, err)
|
||||
OperationOutcome.Failure(err)
|
||||
} else {
|
||||
OperationOutcome.Success(res)
|
||||
}
|
||||
}.get()
|
||||
for ((index, attempt) in attempts.withIndex()) {
|
||||
val (timestamp, value) = attempt
|
||||
if (index > 0) {
|
||||
/* Check the delay for subsequent attempts is correct */
|
||||
val previousAttempt = attempts[index - 1]
|
||||
val expectedTimestamp =
|
||||
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
|
||||
val actualTimestamp = timestamp
|
||||
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
|
||||
Assertions.assertTrue(err < 0.1)
|
||||
}
|
||||
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
|
||||
/*
|
||||
* If the last attempt index is lower than the maximum number of attempts, then
|
||||
* check the outcome handler returns DoNotRetry
|
||||
*/
|
||||
Assertions.assertTrue(outcomeHandler.shouldRetry(value) is OutcomeHandlerResult.DoNotRetry)
|
||||
} else if (index < attempts.size - 1) {
|
||||
/*
|
||||
* If the attempt is not the last attempt check the outcome handler returns Retry
|
||||
*/
|
||||
Assertions.assertTrue(outcomeHandler.shouldRetry(value) is OutcomeHandlerResult.Retry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,8 +15,7 @@
|
||||
<root level="info">
|
||||
<appender-ref ref="console"/>
|
||||
</root>
|
||||
<logger name="io.netty" level="debug"/>
|
||||
<logger name="io.netty.handler.ssl.BouncyCastlePemReader" level="info"/>
|
||||
<logger name="io.netty" level="info"/>
|
||||
<logger name="com.google.code.yanf4j" level="warn"/>
|
||||
<logger name="net.rubyeye.xmemcached" level="warn"/>
|
||||
</configuration>
|
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<rbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:rbcs-client="urn:net.woggioni.rbcs.client"
|
||||
xs:schemaLocation="urn:net.woggioni.rbcs.client jms://net.woggioni.rbcs.client/net/woggioni/rbcs/client/schema/rbcs-client.xsd"
|
||||
>
|
||||
<profile name="profile1" base-url="https://rbcs1.example.com/">
|
||||
<tls-client-auth
|
||||
key-store-file="keystore.pfx"
|
||||
key-store-password="password"
|
||||
key-alias="woggioni@c962475fa38"
|
||||
key-password="key-password"/>
|
||||
<connection write-idle-timeout="PT60S" read-idle-timeout="PT60S" write-timeout="PT0S" read-timeout="PT0S" idle-timeout="PT30S" />
|
||||
<tls-trust-store file="file.pfx" password="password" check-certificate-status="false" verify-server-certificate="true"/>
|
||||
</profile>
|
||||
<profile name="profile2" base-url="https://rbcs2.example.com/">
|
||||
<basic-auth user="user" password="password"/>
|
||||
</profile>
|
||||
</rbcs-client:profiles>
|
@@ -6,8 +6,10 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':gbcs-api')
|
||||
compileOnly catalog.slf4j.api
|
||||
implementation project(':rbcs-api')
|
||||
implementation catalog.slf4j.api
|
||||
implementation catalog.jwo
|
||||
implementation catalog.netty.buffer
|
||||
}
|
||||
|
||||
publishing {
|
12
rbcs-common/src/main/java/module-info.java
Normal file
12
rbcs-common/src/main/java/module-info.java
Normal file
@@ -0,0 +1,12 @@
|
||||
module net.woggioni.rbcs.common {
|
||||
requires java.xml;
|
||||
requires java.logging;
|
||||
requires org.slf4j;
|
||||
requires kotlin.stdlib;
|
||||
requires net.woggioni.jwo;
|
||||
requires io.netty.buffer;
|
||||
requires io.netty.transport;
|
||||
|
||||
provides java.net.spi.URLStreamHandlerProvider with net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory;
|
||||
exports net.woggioni.rbcs.common;
|
||||
}
|
15
rbcs-common/src/main/kotlin/net/woggioni/rbcs/common/BB.kt
Normal file
15
rbcs-common/src/main/kotlin/net/woggioni/rbcs/common/BB.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufAllocator
|
||||
import io.netty.buffer.CompositeByteBuf
|
||||
|
||||
fun extractChunk(buf: CompositeByteBuf, alloc: ByteBufAllocator): ByteBuf {
|
||||
val chunk = alloc.compositeBuffer()
|
||||
for (component in buf.decompose(0, buf.readableBytes())) {
|
||||
chunk.addComponent(true, component.retain())
|
||||
}
|
||||
buf.removeComponents(0, buf.numComponents())
|
||||
buf.clear()
|
||||
return chunk
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import java.io.InputStream
|
||||
|
||||
class ByteBufInputStream(private val buf : ByteBuf) : InputStream() {
|
||||
override fun read(): Int {
|
||||
return buf.takeIf {
|
||||
it.readableBytes() > 0
|
||||
}?.let(ByteBuf::readByte)
|
||||
?.let(Byte::toInt) ?: -1
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
val readableBytes = buf.readableBytes()
|
||||
if(readableBytes == 0) return -1
|
||||
val result = len.coerceAtMost(readableBytes)
|
||||
buf.readBytes(b, off, result)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
buf.release()
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import java.io.OutputStream
|
||||
|
||||
class ByteBufOutputStream(private val buf : ByteBuf) : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
buf.writeByte(b)
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
buf.writeBytes(b, off, len)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
buf.release()
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package net.woggioni.rbcs.common
|
||||
|
||||
class ResourceNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
|
||||
}
|
||||
|
||||
class ModuleNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user