Compare commits

..

26 Commits

Author SHA1 Message Date
37da03c719 added signal handler to native executable
All checks were successful
CI / build (push) Successful in 33m50s
2025-02-25 19:15:48 +08:00
60bc4375cf update lys-catalog version 2025-02-25 15:54:11 +08:00
725fe22b80 added server configuration file documentation 2025-02-25 15:31:26 +08:00
ca18b63f27 added GraalVM native image executable build 2025-02-25 15:30:58 +08:00
23f2a351a6 shared event executor group between server and clients
All checks were successful
CI / build (push) Successful in 3m44s
- improved documentation
- closed memcache client's thread pools
2025-02-24 13:52:20 +08:00
c7d2b89d82 fixed server prefix handling 2025-02-22 15:25:41 +08:00
72c34b57a6 fixed memory leak 2025-02-22 15:25:41 +08:00
619873c4a9 removed readTimeout and writeTimeout from server configuration
added Markdown documentation
2025-02-22 15:25:36 +08:00
591f6e2af4 parametrized password hashing algorithm for basic authentication 2025-02-20 16:46:15 +08:00
ad00ebee9b made logback configuration file overridable in Docker image without changing ENTRYPOINT
All checks were successful
CI / build (push) Successful in 3m30s
2025-02-20 13:39:27 +08:00
adf8a0cf24 added documentation for rbcs-servlet
All checks were successful
CI / build (push) Successful in 1m54s
switched Docker images to serial GC
2025-02-19 23:00:56 +08:00
42eb26a948 optimize imports 2025-02-19 22:40:14 +08:00
f048a60540 implemented streaming request/response streaming
added metadata to cache values

added cache servlet for comparison
2025-02-19 22:37:54 +08:00
0463038aaa first commit with streaming support (buggy and unreliable) 2025-02-13 23:02:08 +08:00
7eca8a270d 0.1.6 release
All checks were successful
CI / build (push) Successful in 3m29s
2025-02-08 00:54:25 +08:00
84d7c977f9 added randomizer to retries 2025-02-07 23:19:13 +08:00
317eadce07 used virtual thread for garbage colection in FileSystemCache
All checks were successful
CI / build (push) Successful in 2m32s
2025-02-07 20:45:29 +08:00
af79e74b95 fixed max message size for memcache backend 2025-02-06 23:09:22 +08:00
78ae21caa4 0.1.4 release
All checks were successful
CI / build (push) Successful in 8m14s
2025-02-06 15:24:00 +08:00
6c0eadb9fb renamed project to "Remote Cache Build Server" (RBCS) 2025-02-06 15:20:50 +08:00
5fef1b932e updated lys-catalog version
All checks were successful
CI / build (push) Successful in 2m32s
2025-02-05 21:49:08 +08:00
5e173dbf62 fixed unit tests 2025-02-05 21:24:10 +08:00
53b24e3d54 improved benchmark accuracy 2025-02-05 19:10:25 +08:00
7d0f24fa58 fixed memory leak in InMemoryCache 2025-02-05 19:09:51 +08:00
1b6cf1bd96 fixed memory leak in memcached plugin 2025-02-05 14:41:11 +08:00
4180df2352 added healthcheck command to client 2025-02-05 00:02:17 +08:00
177 changed files with 6917 additions and 2517 deletions

View File

@@ -31,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"
@@ -39,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 }}
gitea.woggioni.net/woggioni/rbcs:latest
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
target: release
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
-
name: Build gbcs memcache Docker image
name: Build rbcs memcache Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
@@ -52,11 +52,23 @@ jobs:
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/gbcs:memcache
gitea.woggioni.net/woggioni/gbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
gitea.woggioni.net/woggioni/rbcs:memcache
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
target: release-memcache
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/gbcs:buildx
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/rbcs:buildx
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/rbcs:buildx
-
name: Build rbcs memcache Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:native
gitea.woggioni.net/woggioni/rbcs:native-${{ steps.retrieve-version.outputs.VERSION }}
target: release-native
- name: Publish artifacts
env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}

2
.gitignore vendored
View File

@@ -4,4 +4,4 @@
# Ignore Gradle build output directory
build
gbcs-cli/native-image/*.json
rbcs-cli/native-image/*.json

20
LICENSE Normal file
View File

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

209
README.md
View File

@@ -0,0 +1,209 @@
# Remote Build Cache Server
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
### Downloading the 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
### Using 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
## Usage
### Configuration
The location of the `rbcs.xml` configuration file depends on the operating system,
Alternatively it can be changed setting the `RBCS_CONFIGURATION_DIR` environmental variable or `net.woggioni.rbcs.conf.dir` Java system property
to the directory that contain the `rbcs.xml` file.
The server configuration file follows the XML format and uses XML schema for validation
(you can find the schema for the main configuration file [here](https://gitea.woggioni.net/woggioni/rbcs/src/branch/master/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs.xsd)).
The configuration values are enclosed inside XML attribute and support system property / environmental variable interpolation.
As an example, you can configure RBCS to read the server port number from the `RBCS_SERVER_PORT` environmental variable
and the bind address from the `rbc.bind.address` JVM system property with.
Full documentation for all tags and attributes is available [here](doc/server_configuration.md).
### Plugins
If you want to use memcache as a storage backend you'll also need to download [the memcache plugin](https://gitea.woggioni.net/woggioni/-/packages/maven/net.woggioni:rbcs-server-memcache/)
Plugins need to be stored in a folder named `plugins` in the located server's working directory
(the directory where the server process is started). They are shipped as TAR archives, so you need to extract
the content of the archive into the `plugins` directory for the server to pick them up.
### Using RBCS with Gradle
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.
### Using 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
## FAQ
### Why should I use a build cache?
#### Build Caches Improve Build & Test Performance
Building software consists of a number of steps, like compiling sources, executing tests, and linking binaries. Weve seen that a binary artifact repository helps when such a step requires an external component by downloading the artifact from the repository rather than building it locally.
However, there are many additional steps in this build process which can be optimized to reduce the build time. An obvious strategy is to avoid executing build steps which dominate the total build time when these build steps are not needed.
Most build times are dominated by the testing step.
While binary repositories cannot capture the outcome of a test build step (only the test reports
when included in binary artifacts), build caches are designed to eliminate redundant executions
for every build step. Moreover, it generalizes the concept of avoiding work associated with any
incremental step of the build, including test execution, compilation and resource processing.
The mechanism itself is comparable to a pure function. That is, given some inputs such as source
files and environment parameters we know that the output is always going to be the same.
As a result, we can cache it and retrieve it based on a simple cryptographic hash of the inputs.
Build caching is supported natively by some build tools.
#### Improve CI builds with a remote build cache
When analyzing the role of a build cache it is important to take into account the granularity
of the changes that it caches. Imagine a full build for a project with 40 to 50 modules
which fails at the last step (deployment) because the staging environment is temporarily unavailable.
Although the vast majority of the build steps (potentially thousands) succeed,
the change can not be deployed to the staging environment.
Without a build cache one typically relies on a very complex CI configuration to reuse build step outputs
or would have to repeat the full build once the environment is available.
Some build tools dont support incremental builds properly. For example, outputs of a build started
from scratch may vary when compared to subsequent builds that rely on the initial builds output.
As a result, to preserve build integrity, its crucial to rebuild from scratch, or cleanly, in this
scenario.
With a build cache, only the last step needs to be executed and the build can be re-triggered
when the environment is back online. This automatically saves all of the time and
resources required across the different build steps which were successfully executed.
Instead of executing the intermediate steps, the build tool pulls the outputs from the build cache,
avoiding a lot of redundant work
#### Share outputs with a remote build cache
One of the most important advantages of a remote build cache is the ability to share build outputs.
In most CI configurations, for example, a number of pipelines are created.
These may include one for building the sources, one for testing, one for publishing the outcomes
to a remote repository, and other pipelines to test on different platforms.
There are even situations where CI builds partially build a project (i.e. some modules and not others).
Most of those pipelines share a lot of intermediate build steps. All builds which perform testing
require the binaries to be ready. All publishing builds require all previous steps to be executed.
And because modern CI infrastructure means executing everything in containerized (isolated) environments,
significant resources are wasted by repeatedly building the same intermediate artifacts.
A remote build cache greatly reduces this overhead by orders of magnitudes because it provides a way
for all those pipelines to share their outputs. After all, there is no point recreating an output that
is already available in the cache.
Because there are inherent dependencies between software components of a build,
introducing a build cache dramatically reduces the impact of exploding a component into multiple pieces,
allowing for increased modularity without increased overhead.
#### Make local developers more efficient with remote build caches
It is common for different teams within a company to work on different modules of a single large
application. In this case, most teams dont care about building the other parts of the software.
By introducing a remote cache developers immediately benefit from pre-built artifacts when checking out code.
Because it has already been built on CI, they dont have to do it locally.
Introducing a remote cache is a huge benefit for those developers. Consider that a typical developers
day begins by performing a code checkout. Most likely the checked out code has already been built on CI.
Therefore, no time is wasted running the first build of the day. The remote cache provides all of the
intermediate artifacts needed. And, in the event local changes are made, the remote cache still leverages
partial cache hits for projects which are independent. As other developers in the organization request
CI builds, the remote cache continues to populate, increasing the likelihood of these remote cache hits
across team members.

View File

@@ -14,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 {
@@ -24,7 +22,6 @@ allprojects { subproject ->
url = getProperty('gitea.maven.url')
content {
includeModule 'net.woggioni', 'jwo'
includeModule 'net.woggioni', 'xmemcached'
includeGroup 'com.lys'
}
}
@@ -41,7 +38,7 @@ allprojects { subproject ->
withSourcesJar()
modularity.inferModulePath = true
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(23)
vendor = JvmVendorSpec.ORACLE
}
}

View File

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

178
doc/server_configuration.md Normal file
View File

@@ -0,0 +1,178 @@
### 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.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" />
<!--cache xs:type="rbcs:fileSystemCacheType" max-age="P7D" enable-compression="false" path="${sys:java.io.tmpdir}/rbcs"/-->
<authorization>
<users>
<user name="user1" password="II+qeNLft2pZ/JVNo9F7jpjM/BqEcfsJW27NZ6dPVs8tAwHbxrJppKYsbL7J/SMl">
<quota calls="100" period="PT1S"/>
</user>
<user name="user2" password="v6T9+q6/VNpvLknji3ixPiyz2YZCQMXj2FN7hvzbfc2Ig+IzAHO0iiBCH9oWuBDq"/>
<anonymous>
<quota calls="10" period="PT60S" initial-available-calls="10" max-available-calls="10"/>
</anonymous>
</users>
<groups>
<group name="readers">
<users>
<anonymous/>
</users>
<roles>
<reader/>
</roles>
</group>
<group name="writers">
<users>
<user ref="user1"/>
<user ref="user2"/>
</users>
<roles>
<reader/>
<writer/>
</roles>
</group>
</groups>
</authorization>
<authentication>
<basic/>
</authentication>
</rbcs:server>
```

View File

@@ -4,13 +4,18 @@ USER luser
WORKDIR /home/luser
FROM base-release AS release
ADD gbcs-cli-envelope-*.jar gbcs.jar
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/gbcs.jar", "server"]
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-memcache
ADD --chown=luser:luser gbcs-cli-envelope-*.jar gbcs.jar
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-server-memcache*.tar
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar
WORKDIR /home/luser
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:+ZGenerational", "-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 scratch AS release-native
ADD rbcs-cli.upx rbcs-cli
ENTRYPOINT ["./rbcs-cli"]

View File

@@ -18,8 +18,8 @@ configurations {
}
dependencies {
docker project(path: ':gbcs-cli', configuration: 'release')
docker project(path: ':gbcs-server-memcache', 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,26 +30,29 @@ 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> dockerTagMemcache = tasks.register('dockerTagMemcacheImage', DockerTagImage) {
group = 'docker'
repository = 'gitea.woggioni.net/woggioni/gbcs'
imageId = 'gitea.woggioni.net/woggioni/gbcs:memcache'
repository = 'gitea.woggioni.net/woggioni/rbcs'
imageId = 'gitea.woggioni.net/woggioni/rbcs:memcache'
tag = "${version}-memcache"
}
@@ -63,5 +66,3 @@ Provider<DockerPushImage> dockerPush = tasks.register('dockerPushImage', DockerP
}
images = [dockerTag.flatMap{ it.tag }, dockerTagMemcache.flatMap{ it.tag }]
}

View File

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

View File

@@ -1,14 +0,0 @@
package net.woggioni.gbcs.api;
import io.netty.buffer.ByteBuf;
import net.woggioni.gbcs.api.exception.ContentTooLargeException;
import java.nio.channels.ReadableByteChannel;
import java.util.concurrent.CompletableFuture;
public interface Cache extends AutoCloseable {
CompletableFuture<ReadableByteChannel> get(String key);
CompletableFuture<Void> put(String key, ByteBuf content) throws ContentTooLargeException;
}

View File

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

View File

@@ -1,101 +0,0 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
alias catalog.plugins.envelope
alias catalog.plugins.sambal
alias catalog.plugins.graalvm.native.image
alias catalog.plugins.graalvm.jlink
alias catalog.plugins.jpms.check
id 'maven-publish'
}
import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
import net.woggioni.gradle.graalvm.NativeImagePlugin
import net.woggioni.gradle.graalvm.NativeImageTask
import net.woggioni.gradle.graalvm.JlinkPlugin
import net.woggioni.gradle.graalvm.JlinkTask
Property<String> mainModuleName = objects.property(String.class)
mainModuleName.set('net.woggioni.gbcs.cli')
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 = mainModuleName
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 project(':gbcs-server')
// runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic
// runtimeOnly catalog.slf4j.simple
}
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
// systemProperties['log.config.source'] = 'net/woggioni/gbcs/cli/logging.properties'
// systemProperties['java.util.logging.config.file'] = 'classpath:net/woggioni/gbcs/cli/logging.properties'
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/cli/logback.xml'
systemProperties['io.netty.leakDetectionLevel'] = 'DISABLED'
// systemProperties['org.slf4j.simpleLogger.showDateTime'] = 'true'
// systemProperties['org.slf4j.simpleLogger.defaultLogLevel'] = 'debug'
// systemProperties['org.slf4j.simpleLogger.log.com.google.code.yanf4j'] = 'warn'
// systemProperties['org.slf4j.simpleLogger.log.net.rubyeye.xmemcached'] = 'warn'
// systemProperties['org.slf4j.simpleLogger.dateTimeFormat'] = 'yyyy-MM-dd\'T\'HH:mm:ss.SSSZ'
}
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
mainClass = mainClassName
mainModule = mainModuleName
}
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
mainClass = mainClassName
mainModule = mainModuleName
useMusl = true
buildStaticImage = true
}
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
mainClass = mainClassName
mainModule = 'net.woggioni.gbcs.cli'
}
artifacts {
release(envelopeJarTaskProvider)
}
publishing {
publications {
maven(MavenPublication) {
artifact envelopeJar
}
}
}

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
package net.woggioni.gbcs.cli
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.GetCommand
import net.woggioni.gbcs.cli.impl.commands.PasswordHashCommand
import net.woggioni.gbcs.cli.impl.commands.PutCommand
import net.woggioni.gbcs.cli.impl.commands.ServerCommand
import net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory
import net.woggioni.gbcs.common.contextLogger
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())
addSubcommand(PutCommand())
addSubcommand(GetCommand())
})
System.exit(commandLine.execute(*args))
}
}
@CommandLine.Option(names = ["-V", "--version"], versionHelp = true)
var versionHelp = false
private set
@CommandLine.Spec
private lateinit var spec: CommandSpec
override fun run() {
spec.commandLine().usage(System.out);
}
}

View File

@@ -1,135 +0,0 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GradleBuildCacheClient
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.error
import net.woggioni.gbcs.common.info
import net.woggioni.jwo.JWO
import picocli.CommandLine
import java.security.SecureRandom
import java.time.Duration
import java.time.Instant
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 : 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
@CommandLine.Option(
names = ["-s", "--size"],
description = ["Size of a cache value in bytes"],
paramLabel = "SIZE"
)
private var size = 0x1000
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 = GradleBuildCacheClient(profile)
val entryGenerator = sequence {
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
while (true) {
val key = JWO.bytesToHex(random.nextBytes(16))
val content = random.nextInt().toByte()
val value = ByteArray(size, { _ -> content })
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 * 3)
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).thenApply { entry }
future.whenComplete { result, ex ->
if (ex != null) {
log.error(ex.message, ex)
} else {
completionQueue.put(result)
}
semaphore.release()
completionCounter.incrementAndGet()
}
}
}
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 * 3)
val start = Instant.now()
entries.forEach { entry ->
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 { _, _ ->
completionCounter.incrementAndGet()
semaphore.release()
}
}
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")
}
}
}

View File

@@ -1,48 +0,0 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.converters.InputStreamConverter
import net.woggioni.gbcs.client.GradleBuildCacheClient
import net.woggioni.gbcs.common.contextLogger
import picocli.CommandLine
import java.io.InputStream
@CommandLine.Command(
name = "put",
description = ["Add or replace a value to the cache with the specified key"],
showDefaultValues = true
)
class PutCommand : GbcsCommand() {
private val log = contextLogger()
@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 containing the value to be added (defaults to stdin)"],
paramLabel = "VALUE_FILE",
converter = [InputStreamConverter::class]
)
private var value : InputStream = System.`in`
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")
}
GradleBuildCacheClient(profile).use { client ->
value.use {
client.put(key, it.readAllBytes())
}.get()
}
}
}

View File

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

View File

@@ -1,50 +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:sequence>
<xs:choice>
<xs:element name="no-auth" type="gbcs-client:noAuthType"/>
<xs:element name="basic-auth" type="gbcs-client:basicAuthType"/>
<xs:element name="tls-client-auth" type="gbcs-client:tlsClientAuthType"/>
</xs:choice>
<xs:element name="retry-policy" type="gbcs-client:retryType" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="base-url" type="xs:anyURI" use="required"/>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
<xs:attribute name="connection-timeout" type="xs:duration"/>
</xs:complexType>
<xs:complexType name="noAuthType"/>
<xs:complexType name="basicAuthType">
<xs:attribute name="user" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tlsClientAuthType">
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/>
<xs:attribute name="key-store-password" type="xs:string" use="required"/>
<xs:attribute name="key-alias" type="xs:token" use="required"/>
<xs:attribute name="key-password" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="retryType">
<xs:attribute name="max-attempts" type="xs:positiveInteger" use="required"/>
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S"/>
<xs:attribute name="exp" type="xs:double" default="2.0"/>
</xs:complexType>
</xs:schema>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
xs:schemaLocation="urn:net.woggioni.gbcs.client jms://net.woggioni.gbcs.client/net/woggioni/gbcs/client/schema/gbcs-client.xsd"
>
<profile name="profile1" base-url="https://gbcs1.example.com/">
<tls-client-auth
key-store-file="keystore.pfx"
key-store-password="password"
key-alias="woggioni@c962475fa38"
key-password="key-password"/>
</profile>
<profile name="profile2" base-url="https://gbcs2.example.com/">
<basic-auth user="user" password="password"/>
</profile>
</gbcs-client:profiles>

View File

@@ -1,29 +0,0 @@
package net.woggioni.gbcs.common
import net.woggioni.jwo.JWO
import java.net.URI
import java.net.URL
import java.security.MessageDigest
object GBCS {
fun String.toUrl() : URL = URL.of(URI(this), null)
const val GBCS_NAMESPACE_URI: String = "urn:net.woggioni.gbcs.server"
const val GBCS_PREFIX: String = "gbcs"
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
fun digest(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): ByteArray {
md.update(data)
return md.digest()
}
fun digestString(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): String {
return JWO.bytesToHex(digest(data, md))
}
}

View File

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

View File

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

View File

@@ -1 +0,0 @@
net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory

View File

@@ -1,19 +0,0 @@
import net.woggioni.gbcs.api.CacheProvider;
module net.woggioni.gbcs.server.memcache {
requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.api;
requires net.woggioni.jwo;
requires java.xml;
requires kotlin.stdlib;
requires io.netty.transport;
requires io.netty.codec;
requires io.netty.codec.memcache;
requires io.netty.common;
requires io.netty.buffer;
requires org.slf4j;
provides CacheProvider with net.woggioni.gbcs.server.memcache.MemcacheCacheProvider;
opens net.woggioni.gbcs.server.memcache.schema;
}

View File

@@ -1,23 +0,0 @@
package net.woggioni.gbcs.server.memcache
import io.netty.buffer.ByteBuf
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.server.memcache.client.MemcacheClient
import java.nio.channels.ReadableByteChannel
import java.util.concurrent.CompletableFuture
class MemcacheCache(private val cfg : MemcacheCacheConfiguration) : Cache {
private val memcacheClient = MemcacheClient(cfg)
override fun get(key: String): CompletableFuture<ReadableByteChannel?> {
return memcacheClient.get(key)
}
override fun put(key: String, content: ByteBuf): CompletableFuture<Void> {
return memcacheClient.put(key, content, cfg.maxAge)
}
override fun close() {
memcacheClient.close()
}
}

View File

@@ -1,40 +0,0 @@
package net.woggioni.gbcs.server.memcache
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.common.HostAndPort
import java.time.Duration
data class MemcacheCacheConfiguration(
val servers: List<Server>,
val maxAge: Duration = Duration.ofDays(1),
val maxSize: Int = 0x100000,
val digestAlgorithm: String? = null,
val compressionMode: CompressionMode? = null,
) : Configuration.Cache {
enum class CompressionMode {
/**
* Gzip mode
*/
GZIP,
/**
* Deflate mode
*/
DEFLATE
}
data class Server(
val endpoint : HostAndPort,
val connectionTimeoutMillis : Int?,
val maxConnections : Int
)
override fun materialize() = MemcacheCache(this)
override fun getNamespaceURI() = "urn:net.woggioni.gbcs.server.memcache"
override fun getTypeName() = "memcacheCacheType"
}

View File

@@ -1,242 +0,0 @@
package net.woggioni.gbcs.server.memcache.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.memcache.binary.BinaryMemcacheClientCodec
import io.netty.handler.codec.memcache.binary.BinaryMemcacheObjectAggregator
import io.netty.handler.codec.memcache.binary.BinaryMemcacheOpcodes
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponseStatus
import io.netty.handler.codec.memcache.binary.DefaultFullBinaryMemcacheRequest
import io.netty.handler.codec.memcache.binary.FullBinaryMemcacheRequest
import io.netty.handler.codec.memcache.binary.FullBinaryMemcacheResponse
import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.gbcs.common.ByteBufInputStream
import net.woggioni.gbcs.common.GBCS.digest
import net.woggioni.gbcs.common.HostAndPort
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.server.memcache.MemcacheCacheConfiguration
import net.woggioni.gbcs.server.memcache.MemcacheException
import net.woggioni.jwo.JWO
import java.io.ByteArrayOutputStream
import java.net.InetSocketAddress
import java.nio.channels.Channels
import java.nio.channels.ReadableByteChannel
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.InflaterInputStream
import io.netty.util.concurrent.Future as NettyFuture
class MemcacheClient(private val cfg: MemcacheCacheConfiguration) : AutoCloseable {
private companion object {
@JvmStatic
private val log = contextLogger()
}
private val group: NioEventLoopGroup
private val connectionPool: MutableMap<HostAndPort, ChannelPool> = ConcurrentHashMap()
init {
group = NioEventLoopGroup()
}
private fun newConnectionPool(server: MemcacheCacheConfiguration.Server): FixedChannelPool {
val bootstrap = Bootstrap().apply {
group(group)
channel(NioSocketChannel::class.java)
option(ChannelOption.SO_KEEPALIVE, true)
remoteAddress(InetSocketAddress(server.endpoint.host, server.endpoint.port))
server.connectionTimeoutMillis?.let {
option(ChannelOption.CONNECT_TIMEOUT_MILLIS, it)
}
}
val channelPoolHandler = object : AbstractChannelPoolHandler() {
override fun channelCreated(ch: Channel) {
val pipeline: ChannelPipeline = ch.pipeline()
pipeline.addLast(BinaryMemcacheClientCodec())
pipeline.addLast(BinaryMemcacheObjectAggregator(Integer.MAX_VALUE))
}
}
return FixedChannelPool(bootstrap, channelPoolHandler, server.maxConnections)
}
private fun sendRequest(request: FullBinaryMemcacheRequest): CompletableFuture<FullBinaryMemcacheResponse> {
val server = cfg.servers.let { servers ->
if (servers.size > 1) {
val key = request.key().duplicate()
var checksum = 0
while (key.readableBytes() > 4) {
val byte = key.readInt()
checksum = checksum xor byte
}
while (key.readableBytes() > 0) {
val byte = key.readByte()
checksum = checksum xor byte.toInt()
}
servers[checksum % servers.size]
} else {
servers.first()
}
}
val response = CompletableFuture<FullBinaryMemcacheResponse>()
// Custom handler for processing responses
val pool = connectionPool.computeIfAbsent(server.endpoint) {
newConnectionPool(server)
}
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
override fun operationComplete(channelFuture: NettyFuture<Channel>) {
if (channelFuture.isSuccess) {
val channel = channelFuture.now
val pipeline = channel.pipeline()
channel.pipeline()
.addLast("handler", object : SimpleChannelInboundHandler<FullBinaryMemcacheResponse>() {
override fun channelRead0(
ctx: ChannelHandlerContext,
msg: FullBinaryMemcacheResponse
) {
pipeline.removeLast()
pool.release(channel)
response.complete(msg.retain())
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
val ex = when (cause) {
is DecoderException -> cause.cause!!
else -> cause
}
ctx.close()
pipeline.removeLast()
pool.release(channel)
response.completeExceptionally(ex)
}
})
channel.writeAndFlush(request)
} else {
response.completeExceptionally(channelFuture.cause())
}
}
})
return response
}
private fun encodeExpiry(expiry: Duration): Int {
val expirySeconds = expiry.toSeconds()
return expirySeconds.toInt().takeIf { it.toLong() == expirySeconds }
?: Instant.ofEpochSecond(expirySeconds).epochSecond.toInt()
}
fun get(key: String): CompletableFuture<ReadableByteChannel?> {
val request = (cfg.digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digest(key.toByteArray(), md)
} ?: key.toByteArray(Charsets.UTF_8)).let { digest ->
DefaultFullBinaryMemcacheRequest(Unpooled.wrappedBuffer(digest), null).apply {
setOpcode(BinaryMemcacheOpcodes.GET)
}
}
return sendRequest(request).thenApply { response ->
when (val status = response.status()) {
BinaryMemcacheResponseStatus.SUCCESS -> {
val compressionMode = cfg.compressionMode
val content = response.content()
if (compressionMode != null) {
when (compressionMode) {
MemcacheCacheConfiguration.CompressionMode.GZIP -> {
GZIPInputStream(ByteBufInputStream(content))
}
MemcacheCacheConfiguration.CompressionMode.DEFLATE -> {
InflaterInputStream(ByteBufInputStream(content))
}
}
} else {
ByteBufInputStream(content)
}.let(Channels::newChannel)
}
BinaryMemcacheResponseStatus.KEY_ENOENT -> {
null
}
else -> throw MemcacheException(status)
}
}
}
fun put(key: String, content: ByteBuf, expiry: Duration, cas: Long? = null): CompletableFuture<Void> {
val request = (cfg.digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digest(key.toByteArray(), md)
} ?: key.toByteArray(Charsets.UTF_8)).let { digest ->
val extras = Unpooled.buffer(8, 8)
extras.writeInt(0)
extras.writeInt(encodeExpiry(expiry))
val compressionMode = cfg.compressionMode
val payload = if (compressionMode != null) {
val inputStream = ByteBufInputStream(Unpooled.wrappedBuffer(content))
val baos = ByteArrayOutputStream()
val outputStream = when (compressionMode) {
MemcacheCacheConfiguration.CompressionMode.GZIP -> {
GZIPOutputStream(baos)
}
MemcacheCacheConfiguration.CompressionMode.DEFLATE -> {
DeflaterOutputStream(baos, Deflater(Deflater.DEFAULT_COMPRESSION, false))
}
}
inputStream.use { i ->
outputStream.use { o ->
JWO.copy(i, o)
}
}
Unpooled.wrappedBuffer(baos.toByteArray())
} else {
content
}
DefaultFullBinaryMemcacheRequest(Unpooled.wrappedBuffer(digest), extras, payload).apply {
setOpcode(BinaryMemcacheOpcodes.SET)
cas?.let(this::setCas)
}
}
return sendRequest(request).thenApply { response ->
when(val status = response.status()) {
BinaryMemcacheResponseStatus.SUCCESS -> null
else -> throw MemcacheException(status)
}
}
}
fun shutDown(): NettyFuture<*> {
return group.shutdownGracefully()
}
override fun close() {
shutDown().sync()
}
}

View File

@@ -1 +0,0 @@
net.woggioni.gbcs.server.memcache.MemcacheCacheProvider

View File

@@ -1,30 +0,0 @@
package net.woggioni.gbcs.server
import io.netty.channel.ChannelHandlerContext
import org.slf4j.Logger
import java.net.InetSocketAddress
inline fun Logger.trace(ctx : ChannelHandlerContext, messageBuilder : () -> String) {
log(this, ctx, { isTraceEnabled }, { trace(it) } , messageBuilder)
}
inline fun Logger.debug(ctx : ChannelHandlerContext, messageBuilder : () -> String) {
log(this, ctx, { isDebugEnabled }, { debug(it) } , messageBuilder)
}
inline fun Logger.info(ctx : ChannelHandlerContext, messageBuilder : () -> String) {
log(this, ctx, { isInfoEnabled }, { info(it) } , messageBuilder)
}
inline fun Logger.warn(ctx : ChannelHandlerContext, messageBuilder : () -> String) {
log(this, ctx, { isWarnEnabled }, { warn(it) } , messageBuilder)
}
inline fun Logger.error(ctx : ChannelHandlerContext, messageBuilder : () -> String) {
log(this, ctx, { isErrorEnabled }, { error(it) } , messageBuilder)
}
inline fun log(log : Logger, ctx : ChannelHandlerContext,
filter : Logger.() -> Boolean,
loggerMethod : Logger.(String) -> Unit, messageBuilder : () -> String) {
if(log.filter()) {
val clientAddress = (ctx.channel().remoteAddress() as InetSocketAddress).address.hostAddress
log.loggerMethod(clientAddress + " - " + messageBuilder())
}
}

View File

@@ -1,90 +0,0 @@
package net.woggioni.gbcs.server.auth
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.handler.ssl.SslHandler
import io.netty.handler.ssl.SslHandshakeCompletionEvent
import java.security.KeyStore
import java.security.cert.CertPathValidator
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509Certificate
import java.util.EnumSet
import javax.net.ssl.SSLSession
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class ClientCertificateValidator private constructor(
private val sslHandler: SslHandler,
private val x509TrustManager: X509TrustManager
) : ChannelInboundHandlerAdapter() {
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is SslHandshakeCompletionEvent) {
if (evt.isSuccess) {
val session: SSLSession = sslHandler.engine().session
val clientCertificateChain = session.peerCertificates as Array<X509Certificate>
val authType: String = clientCertificateChain[0].publicKey.algorithm
x509TrustManager.checkClientTrusted(clientCertificateChain, authType)
} else {
// Handle the failure, for example by closing the channel.
}
}
super.userEventTriggered(ctx, evt)
}
companion object {
fun getTrustManager(trustStore: KeyStore?, certificateRevocationEnabled: Boolean): X509TrustManager {
return if (trustStore != null) {
val certificateFactory = CertificateFactory.getInstance("X.509")
val validator = CertPathValidator.getInstance("PKIX").apply {
val rc = revocationChecker as PKIXRevocationChecker
rc.options = EnumSet.of(
PKIXRevocationChecker.Option.NO_FALLBACK
)
}
val params = PKIXParameters(trustStore).apply {
isRevocationEnabled = certificateRevocationEnabled
}
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
val clientCertificateChain = certificateFactory.generateCertPath(chain.toList())
try {
validator.validate(clientCertificateChain, params)
} catch (ex: CertPathValidatorException) {
throw CertificateException(ex)
}
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
throw NotImplementedError()
}
private val acceptedIssuers = trustStore.aliases().asSequence()
.filter(trustStore::isCertificateEntry)
.map(trustStore::getCertificate)
.map { it as X509Certificate }
.toList()
.toTypedArray()
override fun getAcceptedIssuers() = acceptedIssuers
}
} else {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }
.single() as X509TrustManager
}
}
fun of(
sslHandler: SslHandler,
trustStore: KeyStore?,
certificateRevocationEnabled: Boolean
): ClientCertificateValidator {
return ClientCertificateValidator(sslHandler, getTrustManager(trustStore, certificateRevocationEnabled))
}
}
}

View File

@@ -1,130 +0,0 @@
package net.woggioni.gbcs.server.cache
import io.netty.buffer.ByteBuf
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.common.ByteBufInputStream
import net.woggioni.gbcs.common.GBCS.digestString
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.jwo.JWO
import net.woggioni.jwo.LockFile
import java.nio.channels.Channels
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.nio.file.attribute.BasicFileAttributes
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
class FileSystemCache(
val root: Path,
val maxAge: Duration,
val digestAlgorithm: String?,
val compressionEnabled: Boolean,
val compressionLevel: Int
) : Cache {
private companion object {
@JvmStatic
private val log = contextLogger()
}
init {
Files.createDirectories(root)
}
private var nextGc = AtomicReference(Instant.now().plus(maxAge))
override fun get(key: String) = (digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
root.resolve(digest).takeIf(Files::exists)
?.let { file ->
file.takeIf(Files::exists)?.let { file ->
if (compressionEnabled) {
val inflater = Inflater()
Channels.newChannel(
InflaterInputStream(
Channels.newInputStream(
FileChannel.open(
file,
StandardOpenOption.READ
)
), inflater
)
)
} else {
FileChannel.open(file, StandardOpenOption.READ)
}
}
}.also {
gc()
}.let {
CompletableFuture.completedFuture(it)
}
}
override fun put(key: String, content: ByteBuf): CompletableFuture<Void> {
(digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
val file = root.resolve(digest)
val tmpFile = Files.createTempFile(root, null, ".tmp")
try {
Files.newOutputStream(tmpFile).let {
if (compressionEnabled) {
val deflater = Deflater(compressionLevel)
DeflaterOutputStream(it, deflater)
} else {
it
}
}.use {
JWO.copy(ByteBufInputStream(content), it)
}
Files.move(tmpFile, file, StandardCopyOption.ATOMIC_MOVE)
} catch (t: Throwable) {
Files.delete(tmpFile)
throw t
}
}.also {
gc()
}
return CompletableFuture.completedFuture(null)
}
private fun gc() {
val now = Instant.now()
val oldValue = nextGc.getAndSet(now.plus(maxAge))
if (oldValue < now) {
actualGc(now)
}
}
@Synchronized
private fun actualGc(now: Instant) {
Files.list(root).filter {
val creationTimeStamp = Files.readAttributes(it, BasicFileAttributes::class.java)
.creationTime()
.toInstant()
now > creationTimeStamp.plus(maxAge)
}.forEach { file ->
LockFile.acquire(file, false).use {
Files.delete(file)
}
}
}
override fun close() {}
}

View File

@@ -1,27 +0,0 @@
package net.woggioni.gbcs.server.cache
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.common.GBCS
import net.woggioni.jwo.Application
import java.nio.file.Path
import java.time.Duration
data class FileSystemCacheConfiguration(
val root: Path?,
val maxAge: Duration,
val digestAlgorithm : String?,
val compressionEnabled: Boolean,
val compressionLevel: Int,
) : Configuration.Cache {
override fun materialize() = FileSystemCache(
root ?: Application.builder("gbcs").build().computeCacheDirectory(),
maxAge,
digestAlgorithm,
compressionEnabled,
compressionLevel
)
override fun getNamespaceURI() = GBCS.GBCS_NAMESPACE_URI
override fun getTypeName() = "fileSystemCacheType"
}

View File

@@ -1,99 +0,0 @@
package net.woggioni.gbcs.server.cache
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.common.ByteBufInputStream
import net.woggioni.gbcs.common.GBCS.digestString
import net.woggioni.jwo.JWO
import java.io.ByteArrayOutputStream
import java.nio.channels.Channels
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.PriorityBlockingQueue
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
class InMemoryCache(
val maxAge: Duration,
val digestAlgorithm: String?,
val compressionEnabled: Boolean,
val compressionLevel: Int
) : Cache {
private val map = ConcurrentHashMap<String, ByteBuf>()
private class RemovalQueueElement(val key: String, val value : ByteBuf, val expiry : Instant) : Comparable<RemovalQueueElement> {
override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry)
}
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>()
private var running = true
private val garbageCollector = Thread {
while(true) {
val el = removalQueue.take()
val now = Instant.now()
if(now > el.expiry) {
map.remove(el.key, el.value)
} else {
removalQueue.put(el)
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
}
}
}.apply {
start()
}
override fun close() {
running = false
garbageCollector.join()
}
override fun get(key: String) =
(digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key
).let { digest ->
map[digest]
?.let { value ->
if (compressionEnabled) {
val inflater = Inflater()
Channels.newChannel(InflaterInputStream(ByteBufInputStream(value), inflater))
} else {
Channels.newChannel(ByteBufInputStream(value))
}
}
}.let {
CompletableFuture.completedFuture(it)
}
override fun put(key: String, content: ByteBuf) =
(digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
val value = if (compressionEnabled) {
val deflater = Deflater(compressionLevel)
val baos = ByteArrayOutputStream()
DeflaterOutputStream(baos, deflater).use { stream ->
JWO.copy(ByteBufInputStream(content), stream)
}
Unpooled.wrappedBuffer(baos.toByteArray())
} else {
content
}
map[digest] = value
removalQueue.put(RemovalQueueElement(digest, value, Instant.now().plus(maxAge)))
}.let {
CompletableFuture.completedFuture<Void>(null)
}
}

View File

@@ -1,23 +0,0 @@
package net.woggioni.gbcs.server.cache
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.common.GBCS
import java.time.Duration
data class InMemoryCacheConfiguration(
val maxAge: Duration,
val digestAlgorithm : String?,
val compressionEnabled: Boolean,
val compressionLevel: Int,
) : Configuration.Cache {
override fun materialize() = InMemoryCache(
maxAge,
digestAlgorithm,
compressionEnabled,
compressionLevel
)
override fun getNamespaceURI() = GBCS.GBCS_NAMESPACE_URI
override fun getTypeName() = "inMemoryCacheType"
}

View File

@@ -1,148 +0,0 @@
package net.woggioni.gbcs.server.handler
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.DefaultFileRegion
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.DefaultHttpResponse
import io.netty.handler.codec.http.FullHttpRequest
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.HttpResponseStatus
import io.netty.handler.codec.http.HttpUtil
import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.stream.ChunkedNioStream
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.server.debug
import net.woggioni.gbcs.server.warn
import java.nio.channels.FileChannel
import java.nio.file.Path
@ChannelHandler.Sharable
class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
SimpleChannelInboundHandler<FullHttpRequest>() {
private val log = contextLogger()
override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) {
val keepAlive: Boolean = HttpUtil.isKeepAlive(msg)
val method = msg.method()
if (method === HttpMethod.GET) {
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName?.toString() ?: let {
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.NOT_FOUND)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response)
return
}
if (serverPrefix == prefix) {
cache.get(key).thenApply { channel ->
if(channel != null) {
log.debug(ctx) {
"Cache hit for key '$key'"
}
val response = DefaultHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK)
response.headers()[HttpHeaderNames.CONTENT_TYPE] = HttpHeaderValues.APPLICATION_OCTET_STREAM
if (!keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.IDENTITY)
} else {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED)
}
ctx.write(response)
val content : Any = when (channel) {
is FileChannel -> DefaultFileRegion(channel, 0, channel.size())
else -> ChunkedNioStream(channel)
}
if (keepAlive) {
ctx.write(content)
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate())
} else {
ctx.writeAndFlush(content)
.addListener(ChannelFutureListener.CLOSE)
}
} else {
log.debug(ctx) {
"Cache miss for key '$key'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.NOT_FOUND)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response)
}
}.whenComplete { _, ex -> ex?.let(ctx::fireExceptionCaught) }
} else {
log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
ctx.writeAndFlush(response)
}
} else if (method === HttpMethod.PUT) {
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
log.debug(ctx) {
"Added value for key '$key' to build cache"
}
cache.put(key, msg.content().retain()).thenRun {
val response = DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.CREATED,
Unpooled.copiedBuffer(key.toByteArray())
)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes()
ctx.writeAndFlush(response)
}.whenComplete { _, ex ->
ctx.fireExceptionCaught(ex)
}
} else {
log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.BAD_REQUEST)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
ctx.writeAndFlush(response)
}
} else if(method == HttpMethod.TRACE) {
val replayedRequestHead = ctx.alloc().buffer()
replayedRequestHead.writeCharSequence("TRACE ${Path.of(msg.uri())} ${msg.protocolVersion().text()}\r\n", Charsets.US_ASCII)
msg.headers().forEach { (key, value) ->
replayedRequestHead.apply {
writeCharSequence(key, Charsets.US_ASCII)
writeCharSequence(": ", Charsets.US_ASCII)
writeCharSequence(value, Charsets.UTF_8)
writeCharSequence("\r\n", Charsets.US_ASCII)
}
}
replayedRequestHead.writeCharSequence("\r\n", Charsets.US_ASCII)
val requestBody = msg.content()
requestBody.retain()
val responseBody = ctx.alloc().compositeBuffer(2).apply {
addComponents(true, replayedRequestHead)
addComponents(true, requestBody)
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK, responseBody)
response.headers().apply {
set(HttpHeaderNames.CONTENT_TYPE, "message/http")
set(HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes())
}
ctx.writeAndFlush(response)
} else {
log.warn(ctx) {
"Got request with unhandled method '${msg.method().name()}'"
}
val response = DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.METHOD_NOT_ALLOWED)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
ctx.writeAndFlush(response)
}
}
}

View File

@@ -1,99 +0,0 @@
package net.woggioni.gbcs.server.throttling
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.server.GradleBuildCacheServer
import net.woggioni.jwo.Bucket
import net.woggioni.jwo.LongMath
import java.net.InetSocketAddress
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
@Sharable
class ThrottlingHandler(cfg: Configuration) :
ChannelInboundHandlerAdapter() {
private val log = contextLogger()
private val bucketManager = BucketManager.from(cfg)
private val connectionConfiguration = cfg.connection
/**
* If the suggested waiting time from the bucket is lower than this
* amount, then the server will simply wait by itself before sending a response
* instead of replying with 429
*/
private val waitThreshold = minOf(
connectionConfiguration.idleTimeout,
connectionConfiguration.readIdleTimeout,
connectionConfiguration.writeIdleTimeout
).dividedBy(2)
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
val buckets = mutableListOf<Bucket>()
val user = ctx.channel().attr(GradleBuildCacheServer.userAttribute).get()
if (user != null) {
bucketManager.getBucketByUser(user)?.let(buckets::addAll)
}
val groups = ctx.channel().attr(GradleBuildCacheServer.groupAttribute).get() ?: emptySet()
if (groups.isNotEmpty()) {
groups.forEach { group ->
bucketManager.getBucketByGroup(group)?.let(buckets::add)
}
}
if (user == null && groups.isEmpty()) {
bucketManager.getBucketByAddress(ctx.channel().remoteAddress() as InetSocketAddress)?.let(buckets::add)
}
if (buckets.isEmpty()) {
return super.channelRead(ctx, msg)
} else {
handleBuckets(buckets, ctx, msg, true)
}
}
private fun handleBuckets(buckets : List<Bucket>, ctx : ChannelHandlerContext, msg : Any, delayResponse : Boolean) {
var nextAttempt = -1L
for (bucket in buckets) {
val bucketNextAttempt = bucket.removeTokensWithEstimate(1)
if (bucketNextAttempt > nextAttempt) {
nextAttempt = bucketNextAttempt
}
}
if(nextAttempt < 0) {
super.channelRead(ctx, msg)
return
}
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttempt, 100_000_000L) * 100L, ChronoUnit.MILLIS)
if (delayResponse && waitDuration < waitThreshold) {
ctx.executor().schedule({
handleBuckets(buckets, ctx, msg, false)
}, waitDuration.toMillis(), TimeUnit.MILLISECONDS)
} else {
sendThrottledResponse(ctx, waitDuration)
}
}
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration) {
val response = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.TOO_MANY_REQUESTS
)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
retryAfter.seconds.takeIf {
it > 0
}?.let {
response.headers()[HttpHeaderNames.RETRY_AFTER] = retryAfter.seconds
}
ctx.writeAndFlush(response)
}
}

View File

@@ -1,2 +0,0 @@
net.woggioni.gbcs.server.cache.FileSystemCacheProvider
net.woggioni.gbcs.server.cache.InMemoryCacheProvider

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="8080" incoming-connections-backlog-size="1024"/>
<connection
max-request-size="67108864"
idle-timeout="PT30S"
read-timeout="PT10S"
write-timeout="PT10S"
read-idle-timeout="PT60S"
write-idle-timeout="PT60S"/>
<event-executor use-virtual-threads="true"/>
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authentication>
<none/>
</authentication>
</gbcs:server>

View File

@@ -1,223 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.gbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:gbcs="urn:net.woggioni.gbcs.server"
elementFormDefault="unqualified">
<xs:element name="server" type="gbcs:serverType"/>
<xs:complexType name="serverType">
<xs:sequence minOccurs="0">
<xs:element name="bind" type="gbcs:bindType" maxOccurs="1"/>
<xs:element name="connection" type="gbcs:connectionType" minOccurs="0" maxOccurs="1"/>
<xs:element name="event-executor" type="gbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/>
<xs:element name="cache" type="gbcs:cacheType" maxOccurs="1"/>
<xs:element name="authorization" type="gbcs:authorizationType" minOccurs="0">
<xs:key name="userId">
<xs:selector xpath="users/user"/>
<xs:field xpath="@name"/>
</xs:key>
<xs:keyref name="userRef" refer="gbcs:userId">
<xs:selector xpath="groups/group/users/user"/>
<xs:field xpath="@ref"/>
</xs:keyref>
</xs:element>
<xs:element name="authentication" type="gbcs:authenticationType" minOccurs="0" maxOccurs="1"/>
<xs:element name="tls" type="gbcs:tlsType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="path" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="bindType">
<xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
<xs:attribute name="incoming-connections-backlog-size" type="xs:unsignedInt" use="optional" default="1024"/>
</xs:complexType>
<xs:complexType name="connectionType">
<xs:attribute name="read-timeout" type="xs:duration" use="optional" default="PT0S"/>
<xs:attribute name="write-timeout" type="xs:duration" use="optional" default="PT0S"/>
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S"/>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
<xs:attribute name="max-request-size" type="xs:unsignedInt" use="optional" default="67108864"/>
</xs:complexType>
<xs:complexType name="eventExecutorType">
<xs:attribute name="use-virtual-threads" type="xs:boolean" use="optional" default="true"/>
</xs:complexType>
<xs:complexType name="cacheType" abstract="true"/>
<xs:complexType name="inMemoryCacheType">
<xs:complexContent>
<xs:extension base="gbcs:cacheType">
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="digest" type="xs:token" default="MD5"/>
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
<xs:attribute name="compression-level" type="xs:byte" default="-1"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="fileSystemCacheType">
<xs:complexContent>
<xs:extension base="gbcs:cacheType">
<xs:attribute name="path" type="xs:string" use="required"/>
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="digest" type="xs:token" default="MD5"/>
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
<xs:attribute name="compression-level" type="xs:byte" default="-1"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tlsCertificateAuthorizationType">
<xs:sequence>
<xs:element name="group-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
<xs:element name="user-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="X500NameExtractorType">
<xs:attribute name="attribute-name" type="xs:token"/>
<xs:attribute name="pattern" type="xs:token"/>
</xs:complexType>
<xs:complexType name="authorizationType">
<xs:all>
<xs:element name="users" type="gbcs:usersType"/>
<xs:element name="groups" type="gbcs:groupsType">
<xs:unique name="groupKey">
<xs:selector xpath="group"/>
<xs:field xpath="@name"/>
</xs:unique>
</xs:element>
</xs:all>
</xs:complexType>
<xs:complexType name="authenticationType">
<xs:choice>
<xs:element name="basic"/>
<xs:element name="client-certificate" type="gbcs:tlsCertificateAuthorizationType"/>
<xs:element name="none"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="quotaType">
<xs:attribute name="calls" type="xs:positiveInteger" use="required"/>
<xs:attribute name="period" type="xs:duration" use="required"/>
<xs:attribute name="max-available-calls" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="initial-available-calls" type="xs:unsignedInt" use="optional"/>
</xs:complexType>
<xs:complexType name="anonymousUserType">
<xs:sequence>
<xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userType">
<xs:sequence>
<xs:element name="quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="usersType">
<xs:sequence>
<xs:element name="user" type="gbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="anonymous" type="gbcs:anonymousUserType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="groupsType">
<xs:sequence>
<xs:element name="group" type="gbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="groupType">
<xs:sequence>
<xs:element name="users" type="gbcs:userRefsType" maxOccurs="1" minOccurs="0">
<xs:unique name="userRefWriterKey">
<xs:selector xpath="user"/>
<xs:field xpath="@ref"/>
</xs:unique>
</xs:element>
<xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/>
<xs:element name="user-quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
<xs:element name="group-quota" type="gbcs:quotaType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
</xs:complexType>
<xs:simpleType name="role" final="restriction" >
<xs:restriction base="xs:token">
<xs:enumeration value="READER" />
<xs:enumeration value="WRITER" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="rolesType">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="writer"/>
<xs:element name="reader"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefsType">
<xs:sequence>
<xs:element name="user" type="gbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/>
<xs:element name="anonymous" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefType">
<xs:attribute name="ref" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tlsType">
<xs:all>
<xs:element name="keystore" type="gbcs:keyStoreType" />
<xs:element name="truststore" type="gbcs:trustStoreType" minOccurs="0"/>
</xs:all>
</xs:complexType>
<xs:complexType name="keyStoreType">
<xs:attribute name="file" type="xs:string" use="required"/>
<xs:attribute name="password" type="xs:string"/>
<xs:attribute name="key-alias" type="xs:string" use="required"/>
<xs:attribute name="key-password" type="xs:string"/>
</xs:complexType>
<xs:complexType name="trustStoreType">
<xs:attribute name="file" type="xs:string" use="required"/>
<xs:attribute name="password" type="xs:string"/>
<xs:attribute name="check-certificate-status" type="xs:boolean"/>
<xs:attribute name="require-client-certificate" type="xs:boolean" use="optional" default="false"/>
</xs:complexType>
<xs:complexType name="propertiesType">
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="gbcs:propertyType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="propertyType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="key" type="xs:string" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="hostAndPortType">
<xs:attribute name="host" type="xs:string" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
</xs:complexType>
</xs:schema>

View File

@@ -1,30 +0,0 @@
package net.woggioni.gbcs.server.test.utils;
import net.woggioni.jwo.JWO;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
public class NetworkUtils {
private static final int MAX_ATTEMPTS = 50;
public static int getFreePort() {
int count = 0;
while(count < MAX_ATTEMPTS) {
try (ServerSocket serverSocket = new ServerSocket(0, 50, InetAddress.getLocalHost())) {
final var candidate = serverSocket.getLocalPort();
if (candidate > 0) {
return candidate;
} else {
JWO.newThrowable(RuntimeException.class, "Got invalid port number: %d", candidate);
throw new RuntimeException("Error trying to find an open port");
}
} catch (IOException ignored) {
++count;
}
}
throw new RuntimeException("Error trying to find an open port");
}
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs.server"
xmlns:gbcs-memcache="urn:net.woggioni.gbcs.server.memcache"
xs:schemaLocation="urn:net.woggioni.gbcs.server.memcache jpms://net.woggioni.gbcs.server.memcache/net/woggioni/gbcs/server/memcache/schema/gbcs-memcache.xsd urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443" incoming-connections-backlog-size="50"/>
<connection
write-timeout="PT25M"
read-timeout="PT20M"
read-idle-timeout="PT10M"
write-idle-timeout="PT11M"
idle-timeout="PT30M"
max-request-size="101325"/>
<event-executor use-virtual-threads="false"/>
<cache xs:type="gbcs-memcache:memcacheCacheType" max-age="P7D" max-size="101325" digest="SHA-256">
<server host="127.0.0.1" port="11211" max-connections="10" connection-timeout="PT20S"/>
</cache>
<authentication>
<none/>
</authentication>
</gbcs:server>

View File

@@ -2,11 +2,10 @@ org.gradle.configuration-cache=false
org.gradle.parallel=true
org.gradle.caching=true
gbcs.version = 0.1.2
rbcs.version = 0.2.0
lys.version = 2025.01.31
lys.version = 2025.02.25
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
docker.registry.url=gitea.woggioni.net
jpms-check.configurationName = runtimeClasspath

View File

@@ -5,7 +5,9 @@ plugins {
}
dependencies {
api catalog.netty.common
api catalog.netty.buffer
api catalog.netty.handler
}
publishing {

View 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;
}

View File

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

View File

@@ -0,0 +1,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
);
}

View File

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

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.api;
package net.woggioni.rbcs.api;
import lombok.EqualsAndHashCode;
@@ -35,8 +35,6 @@ public class Configuration {
@Value
public static class Connection {
Duration readTimeout;
Duration writeTimeout;
Duration idleTimeout;
Duration readIdleTimeout;
Duration writeIdleTimeout;
@@ -85,17 +83,6 @@ public class Configuration {
Group extract(X509Certificate cert);
}
@Value
public static class Throttling {
KeyStore keyStore;
TrustStore trustStore;
boolean verifyClients;
}
public enum ClientCertificate {
REQUIRED, OPTIONAL
}
@Value
public static class Tls {
KeyStore keyStore;
@@ -135,7 +122,7 @@ public class Configuration {
}
public interface Cache {
net.woggioni.gbcs.api.Cache materialize();
CacheHandlerFactory materialize();
String getNamespaceURI();
String getTypeName();
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.api;
package net.woggioni.rbcs.api;
public enum Role {
Reader, Writer

View File

@@ -1,6 +1,6 @@
package net.woggioni.gbcs.api.exception;
package net.woggioni.rbcs.api.exception;
public class CacheException extends GbcsException {
public class CacheException extends RbcsException {
public CacheException(String message, Throwable cause) {
super(message, cause);
}

View File

@@ -1,6 +1,6 @@
package net.woggioni.gbcs.api.exception;
package net.woggioni.rbcs.api.exception;
public class ConfigurationException extends GbcsException {
public class ConfigurationException extends RbcsException {
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}

View File

@@ -1,6 +1,6 @@
package net.woggioni.gbcs.api.exception;
package net.woggioni.rbcs.api.exception;
public class ContentTooLargeException extends GbcsException {
public class ContentTooLargeException extends RbcsException {
public ContentTooLargeException(String message, Throwable cause) {
super(message, cause);
}

View File

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

View File

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

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

@@ -0,0 +1,148 @@
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.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"
setClasspath(configurations.configureNativeImageRuntimeClasspath + sourceSets.graal.output.classesDirs)
mergeConfiguration = false
systemProperty('logback.configurationFile', 'classpath:net/woggioni/rbcs/cli/logback.xml')
systemProperty('io.netty.leakDetectionLevel', 'DISABLED')
modularity.inferModulePath = false
enabled = false
}
nativeImage {
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"
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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"] }]
}
]

View File

@@ -0,0 +1,74 @@
{
"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/server/rbcs-default.xml\\E"
},
{
"pattern": "\\Qnet/woggioni/rbcs/server/schema/rbcs.xsd\\E"
},
{
"pattern": "\\Qnet/woggioni/rbcs/client/schema/rbcs-client.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": [
""
]
}
]
}

View File

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

View File

@@ -0,0 +1,161 @@
package net.woggioni.rbcs.cli.graal
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.HealthCheckCommand
import net.woggioni.rbcs.client.RemoteBuildCacheClient
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.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
object GraalNativeImageConfiguration {
@JvmStatic
fun main(vararg args : String) {
val serverDoc = RemoteBuildCacheServer.DEFAULT_CONFIGURATION_URL.openStream().use {
Xml.parseXml(RemoteBuildCacheServer.DEFAULT_CONFIGURATION_URL, it)
}
Parser.parse(doc)
val clientDoc = RemoteBuildCacheClient.Configuration.openStream().use {
Xml.parseXml(RemoteBuildCacheServer.DEFAULT_CONFIGURATION_URL, it)
}
Parser.parse(doc)
val PASSWORD = "password"
val readersGroup = Configuration.Group("readers", setOf(Role.Reader), 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 = RemoteBuildCacheClient.Configuration.Profile(
URI.create("http://127.0.0.1:$serverPort/"),
null,
RemoteBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials("user3", PASSWORD),
Duration.ofSeconds(3),
10,
true,
RemoteBuildCacheClient.Configuration.RetryPolicy(
3,
1000,
1.2
),
RemoteBuildCacheClient.Configuration.TrustStore(null, null, false, false)
)
HealthCheckCommand.run(clientProfile)
BenchmarkCommand.run(
clientProfile,
1000,
0x100,
true
)
serverHandle.sendShutdownSignal()
try {
serverHandle.get()
} catch (ee : ExecutionException) {
}
}
RemoteBuildCacheServerCli.main("--help")
}
}

View File

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

View File

@@ -0,0 +1,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.contextLogger
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 = contextLogger()
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);
}
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.cli.impl
package net.woggioni.rbcs.cli.impl
import picocli.CommandLine
import java.util.jar.Attributes

View File

@@ -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

View File

@@ -0,0 +1,185 @@
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.jwo.JWO
import net.woggioni.jwo.LongMath
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.cli.impl.converters.ByteSizeConverter
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.error
import net.woggioni.rbcs.common.info
import picocli.CommandLine
import java.security.SecureRandom
import java.time.Duration
import java.time.Instant
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 run(profile : RemoteBuildCacheClient.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")
}
run(
profile,
numberOfEntries,
size,
randomValues
)
}
}

View File

@@ -1,24 +1,24 @@
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.GradleBuildCacheClient
import net.woggioni.jwo.Application
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import picocli.CommandLine
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"],
@@ -28,8 +28,8 @@ class ClientCommand(app : Application) : GbcsCommand() {
)
var profileName : String? = null
val configuration : GradleBuildCacheClient.Configuration by lazy {
GradleBuildCacheClient.Configuration.parse(configurationFile)
val configuration : RemoteBuildCacheClient.Configuration by lazy {
RemoteBuildCacheClient.Configuration.parse(configurationFile)
}
override fun run() {

View File

@@ -1,8 +1,8 @@
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.GradleBuildCacheClient
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine
import java.nio.file.Files
import java.nio.file.Path
@@ -12,8 +12,10 @@ import java.nio.file.Path
description = ["Fetch a value from the cache with the specified key"],
showDefaultValues = true
)
class GetCommand : GbcsCommand() {
private val log = contextLogger()
class GetCommand : RbcsCommand() {
companion object{
private val log = createLogger<GetCommand>()
}
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
@@ -38,7 +40,7 @@ class GetCommand : GbcsCommand() {
clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
}
GradleBuildCacheClient(profile).use { client ->
RemoteBuildCacheClient(profile).use { client ->
client.get(key).thenApply { value ->
value?.let {
(output?.let(Files::newOutputStream) ?: System.out).use {

View File

@@ -0,0 +1,52 @@
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.rbcs.cli.impl.RbcsCommand
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 run(profile : RemoteBuildCacheClient.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")
}
run(profile)
}
}

View File

@@ -1,9 +1,9 @@
package net.woggioni.gbcs.cli.impl.commands
package net.woggioni.rbcs.cli.impl.commands
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter
import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
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"],

View File

@@ -0,0 +1,101 @@
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.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>()
}
@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()
}
inputStream.use {
client.put(actualKey, it.readAllBytes(), CacheValueMetadata(contentDisposition, mimeType))
}.get()
println(profile.serverURI.resolve(actualKey))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -4,11 +4,13 @@ plugins {
}
dependencies {
implementation project(':gbcs-api')
implementation project(':gbcs-common')
implementation catalog.picocli
implementation project(':rbcs-api')
implementation project(':rbcs-common')
implementation catalog.slf4j.api
implementation catalog.netty.buffer
implementation catalog.netty.handler
implementation catalog.netty.transport
implementation catalog.netty.common
implementation catalog.netty.codec.http
testRuntimeOnly catalog.logback.classic

View File

@@ -1,4 +1,4 @@
module net.woggioni.gbcs.client {
module net.woggioni.rbcs.client {
requires io.netty.handler;
requires io.netty.codec.http;
requires io.netty.transport;
@@ -6,12 +6,12 @@ module net.woggioni.gbcs.client {
requires io.netty.common;
requires io.netty.buffer;
requires java.xml;
requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.api;
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;
}

View File

@@ -1,10 +1,12 @@
package net.woggioni.gbcs.client
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
@@ -28,13 +30,19 @@ 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.gbcs.client.impl.Parser
import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.debug
import net.woggioni.gbcs.common.trace
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.client.impl.Parser
import net.woggioni.rbcs.common.RBCS.loadKeystore
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.trace
import java.io.IOException
import java.net.InetSocketAddress
import java.net.URI
import java.nio.file.Files
@@ -44,14 +52,21 @@ import java.security.cert.X509Certificate
import java.time.Duration
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.random.Random
import io.netty.util.concurrent.Future as NettyFuture
class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
companion object{
private val log = createLogger<RemoteBuildCacheClient>()
}
class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
private val group: NioEventLoopGroup
private var sslContext: SslContext
private val log = contextLogger()
private val sslContext: SslContext
private val pool: ChannelPool
data class Configuration(
@@ -66,18 +81,36 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
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 readTimeout: Duration,
val writeTimeout: Duration,
val idleTimeout: Duration,
val readIdleTimeout: Duration,
val writeIdleTimeout: Duration
)
data class Profile(
val serverURI: URI,
val connection: Connection?,
val authentication: Authentication?,
val connectionTimeout: Duration?,
val maxConnections: Int,
val compressionEnabled: Boolean,
val retryPolicy: RetryPolicy?,
val tlsTruststore : TrustStore?
)
companion object {
@@ -93,10 +126,33 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
group = NioEventLoopGroup()
sslContext = SslContextBuilder.forClient().also { builder ->
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
builder.keyManager(
tlsClientAuthenticationCredentials.key,
*tlsClientAuthenticationCredentials.certificateChain
)
builder.apply {
keyManager(
tlsClientAuthenticationCredentials.key,
*tlsClientAuthenticationCredentials.certificateChain
)
profile.tlsTruststore?.let { trustStore ->
if(!trustStore.verifyServerCertificate) {
trustManager(object : X509TrustManager {
override fun checkClientTrusted(certChain: Array<out X509Certificate>, p1: String?) {
}
override fun checkServerTrusted(certChain: Array<out X509Certificate>, p1: String?) {
}
override fun getAcceptedIssuers() = null
})
} else {
trustStore.file?.let {
val ts = loadKeystore(it, trustStore.password)
val trustManagerFactory: TrustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(ts)
trustManager(trustManagerFactory)
}
}
}
}
}
}.build()
@@ -141,18 +197,50 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
}
override fun channelCreated(ch: Channel) {
val connectionId = connectionCount.getAndIncrement()
val connectionId = connectionCount.incrementAndGet()
log.debug {
"Created connection $connectionId, total number of active connections: $connectionId"
"Created connection ${ch.id().asShortText()}, total number of active connections: $connectionId"
}
ch.closeFuture().addListener {
val activeConnections = connectionCount.decrementAndGet()
log.debug {
"Closed connection $connectionId, total number of active connections: $activeConnections"
"Closed connection ${
ch.id().asShortText()
}, total number of active connections: $activeConnections"
}
}
val pipeline: ChannelPipeline = ch.pipeline()
profile.connection?.also { conn ->
val readTimeout = conn.readTimeout.toMillis()
val writeTimeout = conn.writeTimeout.toMillis()
if (readTimeout > 0 || writeTimeout > 0) {
pipeline.addLast(
IdleStateHandler(
false,
readTimeout,
writeTimeout,
0,
TimeUnit.MILLISECONDS
)
)
}
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))
@@ -160,7 +248,9 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
// HTTP handlers
pipeline.addLast("codec", HttpClientCodec())
pipeline.addLast("decompressor", HttpContentDecompressor())
if(profile.compressionEnabled) {
pipeline.addLast("decompressor", HttpContentDecompressor())
}
pipeline.addLast("aggregator", HttpObjectAggregator(134217728))
pipeline.addLast("chunked", ChunkedWriteHandler())
}
@@ -206,6 +296,7 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
retryPolicy.initialDelayMillis.toDouble(),
retryPolicy.exp,
outcomeHandler,
Random.Default,
operation
)
} else {
@@ -213,6 +304,25 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
}
}
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)
@@ -234,9 +344,13 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
}
}
fun put(key: String, content: ByteArray): CompletableFuture<Unit> {
fun put(key: String, content: ByteArray, metadata: CacheValueMetadata): CompletableFuture<Unit> {
return executeWithRetry {
sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content)
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) {
@@ -245,35 +359,83 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
}
}
private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?): CompletableFuture<FullHttpResponse> {
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()
channel.pipeline().addLast("handler", object : SimpleChannelInboundHandler<FullHttpResponse>() {
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
) {
pipeline.removeLast()
pool.release(channel)
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()
pipeline.removeLast()
pool.release(channel)
}
})
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)
@@ -285,15 +447,19 @@ class GradleBuildCacheClient(private val profile: Configuration.Profile) : AutoC
).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.KEEP_ALIVE)
set(
HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
)
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}"

View File

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

View File

@@ -1,9 +1,9 @@
package net.woggioni.gbcs.client.impl
package net.woggioni.rbcs.client.impl
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.client.GradleBuildCacheClient
import net.woggioni.gbcs.common.Xml.Companion.asIterable
import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import java.net.URI
import java.nio.file.Files
@@ -12,12 +12,13 @@ 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): GradleBuildCacheClient.Configuration {
fun parse(document: Document): RemoteBuildCacheClient.Configuration {
val root = document.documentElement
val profiles = mutableMapOf<String, GradleBuildCacheClient.Configuration.Profile>()
val profiles = mutableMapOf<String, RemoteBuildCacheClient.Configuration.Profile>()
for (child in root.asIterable()) {
val tagName = child.localName
@@ -27,8 +28,10 @@ object Parser {
child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
val uri = child.renderAttribute("base-url")?.let(::URI)
?: throw ConfigurationException("base-url attribute is required")
var authentication: GradleBuildCacheClient.Configuration.Authentication? = null
var retryPolicy: GradleBuildCacheClient.Configuration.RetryPolicy? = null
var authentication: RemoteBuildCacheClient.Configuration.Authentication? = null
var retryPolicy: RemoteBuildCacheClient.Configuration.RetryPolicy? = null
var connection : RemoteBuildCacheClient.Configuration.Connection? = null
var trustStore : RemoteBuildCacheClient.Configuration.TrustStore? = null
for (gchild in child.asIterable()) {
when (gchild.localName) {
"tls-client-auth" -> {
@@ -49,7 +52,7 @@ object Parser {
.toList()
.toTypedArray()
authentication =
GradleBuildCacheClient.Configuration.Authentication.TlsClientAuthenticationCredentials(
RemoteBuildCacheClient.Configuration.Authentication.TlsClientAuthenticationCredentials(
key,
certChain
)
@@ -61,7 +64,7 @@ object Parser {
val password = gchild.renderAttribute("password")
?: throw ConfigurationException("password attribute is required")
authentication =
GradleBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials(
RemoteBuildCacheClient.Configuration.Authentication.BasicAuthenticationCredentials(
username,
password
)
@@ -80,12 +83,43 @@ object Parser {
gchild.renderAttribute("exp")
?.let(String::toDouble)
?: 2.0f
retryPolicy = GradleBuildCacheClient.Configuration.RetryPolicy(
retryPolicy = RemoteBuildCacheClient.Configuration.RetryPolicy(
maxAttempts,
initialDelay.toMillis(),
exp.toDouble()
)
}
"connection" -> {
val writeTimeout = gchild.renderAttribute("write-timeout")
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
val readTimeout = gchild.renderAttribute("read-timeout")
?.let(Duration::parse) ?: Duration.of(0, ChronoUnit.SECONDS)
val idleTimeout = gchild.renderAttribute("idle-timeout")
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
val readIdleTimeout = gchild.renderAttribute("read-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
connection = RemoteBuildCacheClient.Configuration.Connection(
readTimeout,
writeTimeout,
idleTimeout,
readIdleTimeout,
writeIdleTimeout,
)
}
"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 = RemoteBuildCacheClient.Configuration.TrustStore(file, password, checkCertificateStatus, verifyServerCertificate)
}
}
}
val maxConnections = child.renderAttribute("max-connections")
@@ -93,16 +127,23 @@ object Parser {
?: 50
val connectionTimeout = child.renderAttribute("connection-timeout")
?.let(Duration::parse)
profiles[name] = GradleBuildCacheClient.Configuration.Profile(
val compressionEnabled = child.renderAttribute("enable-compression")
?.let(String::toBoolean)
?: true
profiles[name] = RemoteBuildCacheClient.Configuration.Profile(
uri,
connection,
authentication,
connectionTimeout,
maxConnections,
retryPolicy
compressionEnabled,
retryPolicy,
trustStore
)
}
}
}
return GradleBuildCacheClient.Configuration(profiles)
return RemoteBuildCacheClient.Configuration(profiles)
}
}

View File

@@ -1,8 +1,10 @@
package net.woggioni.gbcs.client
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>()
@@ -24,8 +26,10 @@ fun <T> executeWithRetry(
initialDelay: Double,
exp: Double,
outcomeHandler: OutcomeHandler<T>,
randomizer : Random?,
cb: () -> CompletableFuture<T>
): CompletableFuture<T> {
val finalResult = cb()
var future = finalResult
var shortCircuit = false
@@ -46,7 +50,7 @@ fun <T> executeWithRetry(
is OutcomeHandlerResult.Retry -> {
val res = CompletableFuture<T>()
val delay = run {
val scheduledDelay = (initialDelay * Math.pow(exp, i.toDouble())).toLong()
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({

View File

@@ -0,0 +1,91 @@
<?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:element name="basic-auth" type="rbcs-client:basicAuthType"/>
<xs:element name="tls-client-auth" type="rbcs-client:tlsClientAuthType"/>
</xs:choice>
<xs:element name="connection" type="rbcs-client:connectionType" minOccurs="0" />
<xs:element name="retry-policy" type="rbcs-client:retryType" minOccurs="0"/>
<xs:element name="tls-trust-store" type="rbcs-client:trustStoreType" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="base-url" type="xs:anyURI" use="required"/>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
<xs:attribute name="connection-timeout" type="xs:duration"/>
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
</xs:complexType>
<xs:complexType name="connectionType">
<xs:attribute name="read-timeout" type="xs:duration" use="optional" default="PT0S"/>
<xs:attribute name="write-timeout" type="xs:duration" use="optional" default="PT0S"/>
<xs:attribute name="idle-timeout" type="xs:duration" use="optional" default="PT30S"/>
<xs:attribute name="read-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
<xs:attribute name="write-idle-timeout" type="xs:duration" use="optional" default="PT60S"/>
</xs:complexType>
<xs:complexType name="noAuthType"/>
<xs:complexType name="basicAuthType">
<xs:attribute name="user" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tlsClientAuthType">
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/>
<xs:attribute name="key-store-password" type="xs:string" use="required"/>
<xs:attribute name="key-alias" type="xs:token" use="required"/>
<xs:attribute name="key-password" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="retryType">
<xs:attribute name="max-attempts" type="xs:positiveInteger" use="required"/>
<xs:attribute name="initial-delay" type="xs:duration" default="PT1S"/>
<xs:attribute name="exp" type="xs:double" default="2.0"/>
</xs:complexType>
<xs:complexType name="trustStoreType">
<xs:attribute name="file" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>
Path to the trustore file
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string">
<xs:annotation>
<xs:documentation>
Trustore file password
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="check-certificate-status" type="xs:boolean">
<xs:annotation>
<xs:documentation>
Whether or not check the certificate validity using CRL/OCSP
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="verify-server-certificate" type="xs:boolean" use="optional" default="true">
<xs:annotation>
<xs:documentation>
If false, the client will blindly trust the provided server certificate
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:schema>

View File

@@ -1,8 +1,8 @@
package net.woggioni.gbcs.client
package net.woggioni.rbcs.client
import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.gbcs.common.contextLogger
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
@@ -89,7 +89,7 @@ class RetryTest {
val random = Random(testArgs.seed)
val future =
executeWithRetry(executor, testArgs.maxAttempt, testArgs.initialDelay, testArgs.exp, outcomeHandler) {
executeWithRetry(executor, testArgs.maxAttempt, testArgs.initialDelay, testArgs.exp, outcomeHandler, null) {
val now = System.nanoTime()
val result = CompletableFuture<Int>()
executor.submit {
@@ -129,7 +129,7 @@ class RetryTest {
previousAttempt.first + testArgs.initialDelay * Math.pow(testArgs.exp, index.toDouble()) * 1e6
val actualTimestamp = timestamp
val err = Math.abs(expectedTimestamp - actualTimestamp) / expectedTimestamp
Assertions.assertTrue(err < 1e-3)
Assertions.assertTrue(err < 1e-2)
}
if (index == attempts.size - 1 && index < testArgs.maxAttempt - 1) {
/*

View File

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

View File

@@ -6,7 +6,7 @@ plugins {
}
dependencies {
implementation project(':gbcs-api')
implementation project(':rbcs-api')
implementation catalog.slf4j.api
implementation catalog.jwo
implementation catalog.netty.buffer

View File

@@ -1,11 +1,12 @@
module net.woggioni.gbcs.common {
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.gbcs.common.GbcsUrlStreamHandlerFactory;
exports net.woggioni.gbcs.common;
provides java.net.spi.URLStreamHandlerProvider with net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory;
exports net.woggioni.rbcs.common;
}

View 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
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.common
package net.woggioni.rbcs.common
import io.netty.buffer.ByteBuf
import java.io.InputStream

View File

@@ -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()
}
}

View File

@@ -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) {
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.common
package net.woggioni.rbcs.common
data class HostAndPort(val host: String, val port: Int = 0) {

View File

@@ -0,0 +1,194 @@
package net.woggioni.rbcs.common
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.slf4j.event.Level
import org.slf4j.spi.LoggingEventBuilder
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun <reified T> createLogger() = LoggerFactory.getLogger(T::class.java)
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())
}
}
fun withMDC(params: Array<Pair<String, String>>, cb: () -> Unit) {
object : AutoCloseable {
override fun close() {
for ((key, _) in params) MDC.remove(key)
}
}.use {
for ((key, value) in params) MDC.put(key, value)
cb()
}
}
inline fun Logger.log(level: Level, channel: Channel, crossinline messageBuilder: (LoggingEventBuilder) -> Unit ) {
if (isEnabledForLevel(level)) {
val params = arrayOf<Pair<String, String>>(
"channel-id-short" to channel.id().asShortText(),
"channel-id-long" to channel.id().asLongText(),
"remote-address" to channel.remoteAddress().toString(),
"local-address" to channel.localAddress().toString(),
)
withMDC(params) {
val builder = makeLoggingEventBuilder(level)
// for ((key, value) in params) {
// builder.addKeyValue(key, value)
// }
messageBuilder(builder)
builder.log()
}
}
}
inline fun Logger.log(level: Level, channel: Channel, crossinline messageBuilder: () -> String) {
log(level, channel) { builder ->
builder.setMessage(messageBuilder())
}
}
inline fun Logger.trace(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.TRACE, ch, messageBuilder)
}
inline fun Logger.debug(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.DEBUG, ch, messageBuilder)
}
inline fun Logger.info(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.INFO, ch, messageBuilder)
}
inline fun Logger.warn(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.WARN, ch, messageBuilder)
}
inline fun Logger.error(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.ERROR, ch, messageBuilder)
}
inline fun Logger.trace(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.TRACE, ctx.channel(), messageBuilder)
}
inline fun Logger.debug(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.DEBUG, ctx.channel(), messageBuilder)
}
inline fun Logger.info(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.INFO, ctx.channel(), messageBuilder)
}
inline fun Logger.warn(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.WARN, ctx.channel(), messageBuilder)
}
inline fun Logger.error(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.ERROR, ctx.channel(), messageBuilder)
}
inline fun Logger.log(level: Level, messageBuilder: () -> String) {
if (isEnabledForLevel(level)) {
makeLoggingEventBuilder(level).log(messageBuilder())
}
}
inline fun Logger.trace(messageBuilder: () -> String) {
if (isTraceEnabled) {
trace(messageBuilder())
}
}
inline fun Logger.debug(messageBuilder: () -> String) {
if (isDebugEnabled) {
debug(messageBuilder())
}
}
inline fun Logger.info(messageBuilder: () -> String) {
if (isInfoEnabled) {
info(messageBuilder())
}
}
inline fun Logger.warn(messageBuilder: () -> String) {
if (isWarnEnabled) {
warn(messageBuilder())
}
}
inline fun Logger.error(messageBuilder: () -> String) {
if (isErrorEnabled) {
error(messageBuilder())
}
}
class LoggingConfig {
init {
val logManager = LogManager.getLogManager()
System.getProperty("log.config.source")?.let withSource@{ source ->
val urls = LoggingConfig::class.java.classLoader.getResources(source)
while (urls.hasMoreElements()) {
val url = urls.nextElement()
url.openStream().use { inputStream ->
logManager.readConfiguration(inputStream)
return@withSource
}
}
Path.of(source).takeIf(Files::exists)
?.let(Files::newInputStream)
?.use(logManager::readConfiguration)
}
}
}

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