Compare commits

...

32 Commits

Author SHA1 Message Date
ce8e93f9d5 updated to netty 4.2.6 and Gradle 9.1.0
All checks were successful
CI / build (push) Successful in 5m51s
2025-09-30 21:40:03 +08:00
94021d94c3 updated Netty to 4.2.4
All checks were successful
CI / build (push) Successful in 3m2s
2025-08-15 10:44:38 +08:00
b3c6f29c0f updated library dependencies
All checks were successful
CI / build (push) Successful in 3m44s
2025-07-29 13:15:42 +08:00
ce7e5bb4a0 added documentation 2025-06-18 09:59:48 +08:00
aeae98d9eb resolved race condition hendling pipelined requests
All checks were successful
CI / build (push) Successful in 2m2s
2025-06-17 23:06:04 +08:00
6cba4d24bb resolved race condition in the client for response lifetime
All checks were successful
CI / build (push) Successful in 2m10s
improved memory usage of the in-memory cache backend
2025-06-17 21:40:48 +08:00
52a1b4c200 moved builds to woryzen 2025-06-13 20:52:27 +08:00
559ad5e528 fixed module-info.java
All checks were successful
CI / build (push) Successful in 6m36s
2025-06-13 20:46:35 +08:00
fd0bd1ee5f added optional key prefix to memcache backend 2025-06-13 17:45:15 +08:00
0e92998f16 downgraded toi GraalVM 23 because of bugs in GraalVM 24 2025-06-13 14:32:25 +08:00
9eef91ebba removed excessive logging 2025-06-13 14:16:57 +08:00
3416c327b9 updated GraalVM configuration 2025-06-13 14:09:38 +08:00
9bdaa0d32e optimize imports 2025-06-13 14:08:46 +08:00
206bcd6319 fixed bug with throttling handler when requests are delayed 2025-06-13 13:50:35 +08:00
3774ab8ef0 updated Netty to 4.2.2
All checks were successful
CI / build (push) Successful in 5m52s
2025-06-10 16:34:16 +08:00
303828392e updated Netty to 4.2.1
All checks were successful
CI / build (push) Successful in 26m57s
2025-05-07 14:46:02 +08:00
5d8cbe34ef updated tomcat configuration 2025-05-01 03:42:37 +08:00
85c0d4a384 update Netty to 4.2.0 2025-04-16 01:06:28 +08:00
ae8817ad2a updated benchmarks 2025-03-24 15:01:56 +08:00
69f215e68f tuned GC parameters in Docker images
All checks were successful
CI / build (push) Successful in 16m35s
2025-03-24 14:42:04 +08:00
222b475223 ensured in-memory-cache is allocated to heap memory
All checks were successful
CI / build (push) Successful in 12m29s
2025-03-11 12:29:43 +08:00
ede515e2ca rebuild native image with wider ISA compatibility
All checks were successful
CI / build (push) Successful in 14m13s
2025-03-10 22:28:55 +08:00
974fdb7a91 added k8s benchmark files
All checks were successful
CI / build (push) Successful in 15m39s
2025-03-10 10:09:30 +08:00
a294229ff0 fixed memory leak in MemcachedCacheHandler 2025-03-09 22:16:03 +08:00
9600dd7e4f solved issue with ignored HttpContent and HttpCacheContent messages in the Netty pipeline
All checks were successful
CI / build (push) Successful in 12m48s
2025-03-09 13:57:52 +08:00
729276a2b1 fixed native image configuration
All checks were successful
CI / build (push) Successful in 14m12s
2025-03-08 14:35:50 +08:00
7ba7070693 fixed server support for request pipelining
All checks were successful
CI / build (push) Successful in 15m33s
2025-03-08 11:07:21 +08:00
59a12d6218 added server support for request pipelining
Some checks failed
CI / build (push) Has been cancelled
2025-03-07 11:53:42 +08:00
fc298de548 made chunk size a global shared parameter between the server and the cache backends 2025-03-06 22:08:19 +08:00
8b639fc0b3 added request pipelining support to RemoteBuildCacheClient 2025-03-06 21:58:53 +08:00
5545f618f9 updated documentation 2025-03-04 10:40:05 +08:00
43c0938d9a added test case 2025-03-04 09:35:26 +08:00
98 changed files with 1736 additions and 907 deletions

View File

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

View File

@@ -52,6 +52,8 @@ jobs:
push: true push: true
pull: true pull: true
tags: | tags: |
gitea.woggioni.net/woggioni/rbcs:latest
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
gitea.woggioni.net/woggioni/rbcs:memcache gitea.woggioni.net/woggioni/rbcs:memcache
gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }} gitea.woggioni.net/woggioni/rbcs:memcache-${{ steps.retrieve-version.outputs.VERSION }}
target: release-memcache target: release-memcache
@@ -66,11 +68,21 @@ jobs:
push: true push: true
pull: true pull: true
tags: | tags: |
gitea.woggioni.net/woggioni/rbcs:latest
gitea.woggioni.net/woggioni/rbcs:${{ steps.retrieve-version.outputs.VERSION }}
gitea.woggioni.net/woggioni/rbcs:native gitea.woggioni.net/woggioni/rbcs:native
gitea.woggioni.net/woggioni/rbcs:native-${{ steps.retrieve-version.outputs.VERSION }} gitea.woggioni.net/woggioni/rbcs:native-${{ steps.retrieve-version.outputs.VERSION }}
target: release-native target: release-native
-
name: Build rbcs jlink Docker image
uses: docker/build-push-action@v5.3.0
with:
context: "docker/build/docker"
platforms: linux/amd64
push: true
pull: true
tags: |
gitea.woggioni.net/woggioni/rbcs:jlink
gitea.woggioni.net/woggioni/rbcs:jlink-${{ steps.retrieve-version.outputs.VERSION }}-jlink
target: release-jlink
- name: Publish artifacts - name: Publish artifacts
env: env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}

View File

@@ -82,6 +82,10 @@ It behaves the same as the jar file but it doesn't require a JVM and it has fast
because of GraalVM's [closed-world assumption](https://www.graalvm.org/latest/reference-manual/native-image/basics/#static-analysis), because of GraalVM's [closed-world assumption](https://www.graalvm.org/latest/reference-manual/native-image/basics/#static-analysis),
the native executable does not supports plugins, so it comes with all plugins embedded into it. the native executable does not supports plugins, so it comes with all plugins embedded into it.
> [!WARNING]
> The native executable is built with `-march=skylake`, so it may fail with SIGILL on x86 CPUs that do not support
> the full skylake instruction set (as a rule of thumb, older than 2015)
## Integration with build tools ## Integration with build tools
### Use RBCS with Gradle ### Use RBCS with Gradle

View File

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

View File

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

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

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

View File

@@ -39,7 +39,6 @@ allprojects { subproject ->
modularity.inferModulePath = true modularity.inferModulePath = true
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(21) languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.GRAAL_VM
} }
} }
@@ -47,6 +46,7 @@ allprojects { subproject ->
testImplementation catalog.junit.jupiter.api testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params testImplementation catalog.junit.jupiter.params
testRuntimeOnly catalog.junit.jupiter.engine testRuntimeOnly catalog.junit.jupiter.engine
testRuntimeOnly catalog.junit.platform.launcher
} }
test { test {

View File

@@ -16,22 +16,22 @@ All test were executed under the following conditions:
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) | | Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|----------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------| |----------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 10 | 3691 | 4037 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 10 | 7867 | 13762 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 100 | 6881 | 7483 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 128 | 100 | 7728 | 14180 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 10 | 3790 | 4069 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 10 | 7964 | 10992 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 100 | 6716 | 7408 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 512 | 100 | 8415 | 12478 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 10 | 3399 | 1974 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 10 | 4268 | 5395 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 100 | 5341 | 6402 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 4096 | 100 | 5585 | 8259 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 10 | 1099 | 1116 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 10 | 1063 | 1185 |
| in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 100 | 1379 | 1703 | | in-memory | Intel Celeron J3455 | 1.00 | 4 | 65536 | 100 | 1522 | 1366 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 10 | 4443 | 5170 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 10 | 11271 | 14092 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 100 | 12813 | 13568 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 128 | 100 | 16064 | 24201 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 10 | 4450 | 4383 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 10 | 11504 | 13077 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 100 | 12212 | 13586 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 512 | 100 | 17379 | 22094 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 10 | 3441 | 3012 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 10 | 9151 | 9489 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 100 | 8982 | 10452 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 4096 | 100 | 13194 | 18268 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 10 | 1391 | 1167 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 10 | 1590 | 1174 |
| in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 100 | 1303 | 1151 | | in-memory | Intel Celeron J3455 | 3.50 | 4 | 65536 | 100 | 1539 | 1561 |
### Filesystem cache backend ### Filesystem cache backend
@@ -42,23 +42,22 @@ TLS: disabled
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) | | Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------| |---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 10 | 1208 | 2048 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 128 | 10 | 1478 | 5771 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 1304 | 2394 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 128 | 100 | 3166 | 8070 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 1408 | 2157 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 512 | 10 | 1717 | 5895 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 1282 | 1888 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 512 | 100 | 1125 | 6564 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 1291 | 1256 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 4096 | 10 | 819 | 2509 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 1170 | 1423 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 4096 | 100 | 1136 | 2365 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 313 | 606 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 65536 | 10 | 584 | 632 |
| filesystem | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 298 | 609 | | filesystem | Intel Celeron J3455 | 1.00 | 0.5 | 65536 | 100 | 529 | 635 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 2195 | 3477 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 128 | 10 | 1227 | 3342 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 2480 | 6207 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 128 | 100 | 1156 | 4035 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 2164 | 3413 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 512 | 10 | 979 | 3294 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 2842 | 6218 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 512 | 100 | 1217 | 3888 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 1302 | 2591 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 4096 | 10 | 535 | 1805 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 2270 | 3045 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 4096 | 100 | 555 | 1910 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 375 | 394 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 65536 | 10 | 301 | 494 |
| filesystem | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 364 | 462 | | filesystem | Intel Celeron J3455 | 3.50 | 0.5 | 65536 | 100 | 353 | 595 |
### Memcache cache backend ### Memcache cache backend
@@ -69,19 +68,19 @@ TLS: disabled
| Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) | | Cache backend | CPU | CPU quota | Memory quota (GB) | Request size (b) | Client connections | PUT (req/s) | GET (req/s) |
|---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------| |---------------|---------------------|-----------|-------------------|------------------|--------------------|-------------|-------------|
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 10 | 2505 | 2578 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 10 | 3380 | 6083 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 3582 | 3935 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 128 | 100 | 3323 | 4998 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 2495 | 2784 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 10 | 3924 | 6086 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 3565 | 3883 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 512 | 100 | 3440 | 5049 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 2174 | 2505 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 10 | 3347 | 5255 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 2937 | 3563 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 4096 | 100 | 3685 | 4693 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 648 | 1074 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 10 | 1304 | 1343 |
| memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 724 | 1548 | | memcache | Intel Celeron J3455 | 1.00 | 0.25 | 65536 | 100 | 1481 | 1541 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 2362 | 2927 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 10 | 4667 | 7984 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 5491 | 6531 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 128 | 100 | 4044 | 8358 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 2125 | 2807 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 10 | 4177 | 7828 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 5173 | 6242 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 512 | 100 | 4079 | 8794 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 1720 | 2397 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 10 | 4588 | 6869 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 3871 | 5859 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 4096 | 100 | 5343 | 7797 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 616 | 1016 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 10 | 1624 | 1317 |
| memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 820 | 1677 | | memcache | Intel Celeron J3455 | 3.50 | 0.25 | 65536 | 100 | 1633 | 1317 |

View File

@@ -24,6 +24,7 @@ Configures connection handling parameters.
- `read-idle-timeout` (optional, default: PT60S): Connection timeout when no reads - `read-idle-timeout` (optional, default: PT60S): Connection timeout when no reads
- `write-idle-timeout` (optional, default: PT60S): Connection timeout when no writes - `write-idle-timeout` (optional, default: PT60S): Connection timeout when no writes
- `max-request-size` (optional, default: 0x4000000): Maximum allowed request body size - `max-request-size` (optional, default: 0x4000000): Maximum allowed request body size
- `chunk-size` (default: 0x10000): Maximum socket write size
#### `<event-executor>` #### `<event-executor>`
Configures event execution settings. Configures event execution settings.
@@ -44,7 +45,6 @@ A simple storage backend that uses an hash map to store data in memory
- `digest` (default: MD5): Key hashing algorithm - `digest` (default: MD5): Key hashing algorithm
- `enable-compression` (default: true): Enable deflate compression - `enable-compression` (default: true): Enable deflate compression
- `compression-level` (default: -1): Compression level (-1 to 9) - `compression-level` (default: -1): Compression level (-1 to 9)
- `chunk-size` (default: 0x10000): Maximum socket write size
##### FileSystem Cache ##### FileSystem Cache
@@ -56,7 +56,6 @@ A storage backend that stores data in a folder on the disk
- `digest` (default: MD5): Key hashing algorithm - `digest` (default: MD5): Key hashing algorithm
- `enable-compression` (default: true): Enable deflate compression - `enable-compression` (default: true): Enable deflate compression
- `compression-level` (default: -1): Compression level - `compression-level` (default: -1): Compression level
- `chunk-size` (default: 0x10000): Maximum in-memory cache value size
#### `<authorization>` #### `<authorization>`
Configures user and group-based access control. Configures user and group-based access control.
@@ -134,8 +133,7 @@ Configures TLS encryption.
idle-timeout="PT10S" idle-timeout="PT10S"
read-idle-timeout="PT20S" read-idle-timeout="PT20S"
write-idle-timeout="PT20S" write-idle-timeout="PT20S"
read-timeout="PT5S" chunk-size="0x1000"/>
write-timeout="PT5S"/>
<event-executor use-virtual-threads="true"/> <event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" /> <cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" enable-compression="false" max-size="0x10000000" />
@@ -147,7 +145,7 @@ Configures TLS encryption.
<!-- uncomment this to use memcache as the storage backend, also make sure you have <!-- uncomment this to use memcache as the storage backend, also make sure you have
the memcache plugin installed in the `plugins` directory if you are using running the memcache plugin installed in the `plugins` directory if you are using running
the jar version of RBCS the jar version of RBCS
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="0x1000" digest="MD5"> <cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" digest="MD5">
<server host="127.0.0.1" port="11211" max-connections="256"/> <server host="127.0.0.1" port="11211" max-connections="256"/>
</cache> </cache>
--> -->

View File

@@ -5,7 +5,7 @@ WORKDIR /home/luser
FROM base-release AS release-vanilla FROM base-release AS release-vanilla
ADD rbcs-cli-envelope-*.jar rbcs.jar ADD rbcs-cli-envelope-*.jar rbcs.jar
ENTRYPOINT ["java", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"] ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=70", "-XX:GCTimeRatio=24", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar"]
FROM base-release AS release-memcache FROM base-release AS release-memcache
ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.jar ADD --chown=luser:luser rbcs-cli-envelope-*.jar rbcs.jar
@@ -14,10 +14,29 @@ WORKDIR /home/luser/plugins
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-server-memcache*.tar
WORKDIR /home/luser WORKDIR /home/luser
ADD logback.xml . ADD logback.xml .
ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:+UseSerialGC", "-XX:GCTimeRatio=24", "-jar", "/home/luser/rbcs.jar", "server"] ENTRYPOINT ["java", "-Dlogback.configurationFile=logback.xml", "-XX:MaxRAMPercentage=70", "-XX:GCTimeRatio=24", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/home/luser/rbcs.jar"]
FROM busybox:musl AS base-native
RUN mkdir -p /var/lib/rbcs /etc/rbcs
RUN adduser -D -u 1000 rbcs -h /var/lib/rbcs
FROM scratch AS release-native FROM scratch AS release-native
ADD rbcs-cli.upx /rbcs/rbcs-cli COPY --from=base-native /etc/passwd /etc/passwd
ENV RBCS_CONFIGURATION_DIR="/rbcs" COPY --from=base-native /etc/rbcs /etc/rbcs
WORKDIR /rbcs COPY --from=base-native /var/lib/rbcs /var/lib/rbcs
ENTRYPOINT ["/rbcs/rbcs-cli"] ADD rbcs-cli.upx /usr/bin/rbcs-cli
ENV RBCS_CONFIGURATION_DIR="/etc/rbcs"
USER rbcs
WORKDIR /var/lib/rbcs
ENTRYPOINT ["/usr/bin/rbcs-cli", "-XX:MaximumHeapSizePercent=70"]
FROM debian:12-slim AS release-jlink
RUN mkdir -p /usr/share/java/rbcs
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/rbcs-cli*.tar -C /usr/share/java/rbcs
ADD --chmod=755 rbcs-cli.sh /usr/local/bin/rbcs-cli
RUN adduser -u 1000 luser
USER luser
WORKDIR /home/luser
ADD logback.xml .
ENV JAVA_OPTS=-XX:-UseJVMCICompiler\ -Dlogback.configurationFile=logback.xml\ -XX:MaxRAMPercentage=70\ -XX:GCTimeRatio=24\ -XX:+UseZGC\ -XX:+ZGenerational
ENTRYPOINT ["/usr/local/bin/rbcs-cli"]

View File

@@ -11,11 +11,15 @@ The `memcache` image is similar to the `vanilla` image, except that it also cont
the `rbcs-server-memcache` plugin in the `plugins` folder, use this image if you don't want to use the `native` the `rbcs-server-memcache` plugin in the `plugins` folder, use this image if you don't want to use the `native`
image and want to use memcache as the cache backend image and want to use memcache as the cache backend
The `native` image contains a native, statically-linked executable created with GraalVM The `native` image contains a native, statically-linked executable created with GraalVM Native Image
that has no userspace dependencies. It also embeds the memcache plugin inside the executable. that has no userspace dependencies. It also embeds the memcache plugin inside the executable.
Use this image for maximum efficiency and minimal memory footprint. Use this image for maximum efficiency and minimal memory footprint.
## Which image shoud I use? The `jlink` image contains a custom Java runtime created with GraalVM's Jlink
that only depends on glibc. It also contains the memcache plugin in the module path.
Use this image for best performance.
## Which image should I use?
The `native` image uses Java's SerialGC, so it's ideal for constrained environment like containers or small servers, The `native` image uses Java's SerialGC, so it's ideal for constrained environment like containers or small servers,
if you have a lot of resources and want to squeeze out the maximum throughput you should consider the if you have a lot of resources and want to squeeze out the maximum throughput you should consider the
`vanilla` or `memcache` image, then choose and fine tune the garbage collector. `vanilla` or `memcache` image, then choose and fine tune the garbage collector.

View File

@@ -29,7 +29,7 @@ Provider<Copy> prepareDockerBuild = tasks.register('prepareDockerBuild', Copy) {
group = 'docker' group = 'docker'
into project.layout.buildDirectory.file('docker') into project.layout.buildDirectory.file('docker')
from(configurations.docker) from(configurations.docker)
from(file('Dockerfile')) from(files('Dockerfile', 'rbcs-cli.sh'))
from(rootProject.file('conf')) { from(rootProject.file('conf')) {
include 'logback.xml' include 'logback.xml'
} }

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

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

View File

@@ -2,9 +2,9 @@ org.gradle.configuration-cache=false
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
rbcs.version = 0.2.0 rbcs.version = 0.3.4
lys.version = 2025.03.03 lys.version = 2025.09.30
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
docker.registry.url=gitea.woggioni.net docker.registry.url=gitea.woggioni.net

Binary file not shown.

View File

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

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,8 @@ public class Configuration {
@NonNull @NonNull
EventExecutor eventExecutor; EventExecutor eventExecutor;
@NonNull @NonNull
RateLimiter rateLimiter;
@NonNull
Connection connection; Connection connection;
Map<String, User> users; Map<String, User> users;
Map<String, Group> groups; Map<String, Group> groups;
@@ -28,6 +30,13 @@ public class Configuration {
Authentication authentication; Authentication authentication;
Tls tls; Tls tls;
@Value
public static class RateLimiter {
boolean delayRequest;
int messageBufferSize;
int maxQueuedMessages;
}
@Value @Value
public static class EventExecutor { public static class EventExecutor {
boolean useVirtualThreads; boolean useVirtualThreads;
@@ -39,6 +48,7 @@ public class Configuration {
Duration readIdleTimeout; Duration readIdleTimeout;
Duration writeIdleTimeout; Duration writeIdleTimeout;
int maxRequestSize; int maxRequestSize;
int chunkSize;
} }
@Value @Value
@@ -133,6 +143,7 @@ public class Configuration {
int incomingConnectionsBacklogSize, int incomingConnectionsBacklogSize,
String serverPath, String serverPath,
EventExecutor eventExecutor, EventExecutor eventExecutor,
RateLimiter rateLimiter,
Connection connection, Connection connection,
Map<String, User> users, Map<String, User> users,
Map<String, Group> groups, Map<String, Group> groups,
@@ -146,6 +157,7 @@ public class Configuration {
incomingConnectionsBacklogSize, incomingConnectionsBacklogSize,
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null, serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
eventExecutor, eventExecutor,
rateLimiter,
connection, connection,
users, users,
groups, groups,

View File

@@ -14,17 +14,26 @@ public sealed interface CacheMessage {
private final String key; private final String key;
} }
@Getter
@RequiredArgsConstructor
abstract sealed class CacheGetResponse implements CacheMessage { abstract sealed class CacheGetResponse implements CacheMessage {
private final String key;
} }
@Getter @Getter
@RequiredArgsConstructor
final class CacheValueFoundResponse extends CacheGetResponse { final class CacheValueFoundResponse extends CacheGetResponse {
private final String key;
private final CacheValueMetadata metadata; private final CacheValueMetadata metadata;
public CacheValueFoundResponse(String key, CacheValueMetadata metadata) {
super(key);
this.metadata = metadata;
}
} }
final class CacheValueNotFoundResponse extends CacheGetResponse { final class CacheValueNotFoundResponse extends CacheGetResponse {
public CacheValueNotFoundResponse(String key) {
super(key);
}
} }
@Getter @Getter

View File

@@ -9,14 +9,10 @@ plugins {
id 'maven-publish' id 'maven-publish'
} }
import net.woggioni.gradle.envelope.EnvelopePlugin
import net.woggioni.gradle.envelope.EnvelopeJarTask import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask import net.woggioni.gradle.envelope.EnvelopePlugin
import net.woggioni.gradle.graalvm.NativeImageTask import net.woggioni.gradle.graalvm.*
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 { sourceSets {
configureNativeImage { configureNativeImage {
@@ -29,6 +25,7 @@ sourceSets {
} }
configurations { configurations {
release { release {
transitive = false transitive = false
canBeConsumed = true canBeConsumed = true
@@ -91,6 +88,10 @@ Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named(EnvelopePlugin.E
} }
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) { tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = "net.woggioni.rbcs.cli.graal.GraalNativeImageConfiguration" mainClass = "net.woggioni.rbcs.cli.graal.GraalNativeImageConfiguration"
classpath = project.files( classpath = project.files(
configurations.configureNativeImageRuntimeClasspath, configurations.configureNativeImageRuntimeClasspath,
@@ -101,6 +102,7 @@ tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfi
systemProperty('io.netty.leakDetectionLevel', 'DISABLED') systemProperty('io.netty.leakDetectionLevel', 'DISABLED')
modularity.inferModulePath = false modularity.inferModulePath = false
enabled = true enabled = true
systemProperty('gradle.tmp.dir', temporaryDir.toString())
} }
nativeImage { nativeImage {
@@ -115,16 +117,36 @@ nativeImage {
linkAtBuildTime = false linkAtBuildTime = false
classpath = project.files(jarTaskProvider, configurations.nativeImage) classpath = project.files(jarTaskProvider, configurations.nativeImage)
compressExecutable = true compressExecutable = true
compressionLevel = 10 compressionLevel = 6
useLZMA = false useLZMA = false
} }
Provider<UpxTask> upxTaskProvider = tasks.named(NativeImagePlugin.UPX_TASK_NAME, UpxTask) { Provider<UpxTask> upxTaskProvider = tasks.named(NativeImagePlugin.UPX_TASK_NAME, UpxTask) {
} }
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) { Provider<JlinkTask> jlinkTaskProvider = tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.GRAAL_VM
}
mainClass = mainClassName mainClass = mainClassName
mainModule = 'net.woggioni.rbcs.cli' mainModule = 'net.woggioni.rbcs.cli'
classpath = project.files(
configurations.configureNativeImageRuntimeClasspath,
sourceSets.configureNativeImage.output
)
additionalModules = [
'net.woggioni.rbcs.server.memcache',
'ch.qos.logback.classic',
'jdk.crypto.ec'
]
compressionLevel = 2
stripDebug = false
}
Provider<Tar> jlinkDistTarTaskProvider = tasks.named(JlinkPlugin.JLINK_DIST_TAR_TASK_NAME, Tar) {
exclude 'lib/libjvmcicompiler.so'
} }
tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) { tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) {
@@ -138,6 +160,7 @@ tasks.named(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, ProcessResources) {
artifacts { artifacts {
release(envelopeJarTaskProvider) release(envelopeJarTaskProvider)
release(upxTaskProvider) release(upxTaskProvider)
release(jlinkDistTarTaskProvider)
} }
publishing { publishing {

View File

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

View File

@@ -46,6 +46,9 @@
{ {
"name":"com.github.luben.zstd.Zstd" "name":"com.github.luben.zstd.Zstd"
}, },
{
"name":"com.jcraft.jzlib.JZlib"
},
{ {
"name":"com.sun.crypto.provider.AESCipher$General", "name":"com.sun.crypto.provider.AESCipher$General",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
@@ -120,6 +123,14 @@
"name":"io.netty.buffer.AbstractReferenceCountedByteBuf", "name":"io.netty.buffer.AbstractReferenceCountedByteBuf",
"fields":[{"name":"refCnt"}] "fields":[{"name":"refCnt"}]
}, },
{
"name":"io.netty.buffer.AdaptivePoolingAllocator$Chunk",
"fields":[{"name":"refCnt"}]
},
{
"name":"io.netty.buffer.AdaptivePoolingAllocator$Magazine",
"fields":[{"name":"nextInLine"}]
},
{ {
"name":"io.netty.channel.AbstractChannelHandlerContext", "name":"io.netty.channel.AbstractChannelHandlerContext",
"fields":[{"name":"handlerState"}] "fields":[{"name":"handlerState"}]
@@ -284,20 +295,13 @@
"fields":[{"name":"producerIndex"}] "fields":[{"name":"producerIndex"}]
}, },
{ {
"name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueConsumerIndexField", "name":"io.netty.util.internal.shaded.org.jctools.queues.MpmcArrayQueueConsumerIndexField",
"fields":[{"name":"consumerIndex"}] "fields":[{"name":"consumerIndex"}]
}, },
{ {
"name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerIndexField", "name":"io.netty.util.internal.shaded.org.jctools.queues.MpmcArrayQueueProducerIndexField",
"fields":[{"name":"producerIndex"}] "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", "name":"java.lang.Object",
"allDeclaredFields":true, "allDeclaredFields":true,
@@ -307,26 +311,14 @@
"name":"java.lang.ProcessHandle", "name":"java.lang.ProcessHandle",
"methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }]
}, },
{
"name":"java.lang.RuntimePermission"
},
{ {
"name":"java.lang.System", "name":"java.lang.System",
"methods":[{"name":"console","parameterTypes":[] }] "methods":[{"name":"console","parameterTypes":[] }]
}, },
{ {
"name":"java.lang.Thread", "name":"java.lang.Thread",
"fields":[{"name":"threadLocalRandomProbe"}] "fields":[{"name":"threadLocalRandomProbe"}],
}, "methods":[{"name":"isVirtual","parameterTypes":[] }]
{
"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", "name":"java.nio.Bits",
@@ -358,18 +350,12 @@
{ {
"name":"java.security.AlgorithmParametersSpi" "name":"java.security.AlgorithmParametersSpi"
}, },
{
"name":"java.security.AllPermission"
},
{ {
"name":"java.security.KeyStoreSpi" "name":"java.security.KeyStoreSpi"
}, },
{ {
"name":"java.security.SecureRandomParameters" "name":"java.security.SecureRandomParameters"
}, },
{
"name":"java.security.SecurityPermission"
},
{ {
"name":"java.sql.Connection" "name":"java.sql.Connection"
}, },
@@ -444,9 +430,6 @@
"name":"java.time.ZonedDateTime", "name":"java.time.ZonedDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
}, },
{
"name":"java.util.PropertyPermission"
},
{ {
"name":"java.util.concurrent.ForkJoinTask", "name":"java.util.concurrent.ForkJoinTask",
"fields":[{"name":"aux"}, {"name":"status"}] "fields":[{"name":"aux"}, {"name":"status"}]
@@ -467,26 +450,19 @@
"name":"java.util.concurrent.atomic.Striped64$Cell", "name":"java.util.concurrent.atomic.Striped64$Cell",
"fields":[{"name":"value"}] "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", "name":"javax.security.auth.x500.X500Principal",
"fields":[{"name":"thisX500Name"}], "fields":[{"name":"thisX500Name"}],
"methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }] "methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }]
}, },
{
"name":"javax.smartcardio.CardPermission"
},
{ {
"name":"jdk.internal.misc.Unsafe", "name":"jdk.internal.misc.Unsafe",
"methods":[{"name":"getUnsafe","parameterTypes":[] }] "methods":[{"name":"getUnsafe","parameterTypes":[] }]
}, },
{
"name":"net.woggioni.rbcs.api.CacheHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{ {
"name":"net.woggioni.rbcs.cli.RemoteBuildCacheServerCli", "name":"net.woggioni.rbcs.cli.RemoteBuildCacheServerCli",
"allDeclaredFields":true, "allDeclaredFields":true,
@@ -552,11 +528,7 @@
}, },
{ {
"name":"net.woggioni.rbcs.client.RemoteBuildCacheClient$sendRequest$1$operationComplete$responseHandler$1", "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"] }] "methods":[{"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
},
{
"name":"net.woggioni.rbcs.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", "name":"net.woggioni.rbcs.server.RemoteBuildCacheServer$HttpChunkContentCompressor",
@@ -588,14 +560,14 @@
"name":"net.woggioni.rbcs.server.exception.ExceptionHandler", "name":"net.woggioni.rbcs.server.exception.ExceptionHandler",
"methods":[{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }] "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", "name":"net.woggioni.rbcs.server.handler.MaxRequestSizeHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }] "methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }]
}, },
{
"name":"net.woggioni.rbcs.server.handler.ReadTriggerDuplexHandler",
"methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }]
},
{ {
"name":"net.woggioni.rbcs.server.handler.ServerHandler", "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"] }] "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"] }]
@@ -619,7 +591,7 @@
{ {
"name":"sun.misc.Unsafe", "name":"sun.misc.Unsafe",
"fields":[{"name":"theUnsafe"}], "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":[] }] "methods":[{"name":"copyMemory","parameterTypes":["java.lang.Object","long","java.lang.Object","long","long"] }, {"name":"getAndAddLong","parameterTypes":["java.lang.Object","long","long"] }, {"name":"getAndSetObject","parameterTypes":["java.lang.Object","long","java.lang.Object"] }, {"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }]
}, },
{ {
"name":"sun.nio.ch.SelectorImpl", "name":"sun.nio.ch.SelectorImpl",

View File

@@ -33,13 +33,9 @@
}, { }, {
"pattern":"\\Qnet/woggioni/rbcs/client/schema/rbcs-client.xsd\\E" "pattern":"\\Qnet/woggioni/rbcs/client/schema/rbcs-client.xsd\\E"
}, { }, {
"pattern":"\\Qnet/woggioni/rbcs/server/rbcs-default.xml\\E" "pattern":"\\Qnet/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd\\E"
}, { }, {
"pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E" "pattern":"\\Qnet/woggioni/rbcs/server/schema/rbcs-server.xsd\\E"
}, {
"pattern":"\\Q/net/woggioni/rbcs/server/memcache/schema/rbcs-memcache.xsd\\E"
}, {
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
}]}, }]},
"bundles":[{ "bundles":[{
"name":"com.sun.org.apache.xerces.internal.impl.xpath.regex.message", "name":"com.sun.org.apache.xerces.internal.impl.xpath.regex.message",

View File

@@ -1,5 +1,12 @@
package net.woggioni.rbcs.cli.graal package net.woggioni.rbcs.cli.graal
import java.io.ByteArrayInputStream
import java.net.URI
import java.nio.file.Path
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.ExecutionException
import java.util.zip.Deflater
import net.woggioni.jwo.NullOutputStream import net.woggioni.jwo.NullOutputStream
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Configuration.User import net.woggioni.rbcs.api.Configuration.User
@@ -9,6 +16,8 @@ import net.woggioni.rbcs.cli.impl.commands.BenchmarkCommand
import net.woggioni.rbcs.cli.impl.commands.GetCommand import net.woggioni.rbcs.cli.impl.commands.GetCommand
import net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand import net.woggioni.rbcs.cli.impl.commands.HealthCheckCommand
import net.woggioni.rbcs.cli.impl.commands.PutCommand import net.woggioni.rbcs.cli.impl.commands.PutCommand
import net.woggioni.rbcs.client.Configuration as ClientConfiguration
import net.woggioni.rbcs.client.impl.Parser as ClientConfigurationParser
import net.woggioni.rbcs.common.HostAndPort import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import net.woggioni.rbcs.common.RBCS import net.woggioni.rbcs.common.RBCS
@@ -18,21 +27,12 @@ import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.configuration.Parser import net.woggioni.rbcs.server.configuration.Parser
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
import java.io.ByteArrayInputStream
import java.net.URI
import java.nio.file.Path
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.ExecutionException
import java.util.zip.Deflater
import net.woggioni.rbcs.client.Configuration as ClientConfiguration
import net.woggioni.rbcs.client.impl.Parser as ClientConfigurationParser
object GraalNativeImageConfiguration { object GraalNativeImageConfiguration {
@JvmStatic @JvmStatic
fun main(vararg args : String) { fun main(vararg args : String) {
val serverURL = URI.create("file:conf/rbcs-client.xml").toURL() val serverURL = URI.create("file:conf/rbcs-server.xml").toURL()
val serverDoc = serverURL.openStream().use { val serverDoc = serverURL.openStream().use {
Xml.parseXml(serverURL, it) Xml.parseXml(serverURL, it)
} }
@@ -71,7 +71,6 @@ object GraalNativeImageConfiguration {
compressionLevel = Deflater.DEFAULT_COMPRESSION, compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false, compressionEnabled = false,
maxSize = 0x1000000, maxSize = 0x1000000,
chunkSize = 0x1000
), ),
FileSystemCacheConfiguration( FileSystemCacheConfiguration(
Path.of(System.getProperty("java.io.tmpdir")).resolve("rbcs"), Path.of(System.getProperty("java.io.tmpdir")).resolve("rbcs"),
@@ -79,7 +78,6 @@ object GraalNativeImageConfiguration {
digestAlgorithm = "MD5", digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION, compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false, compressionEnabled = false,
chunkSize = 0x1000
), ),
MemcacheCacheConfiguration( MemcacheCacheConfiguration(
listOf(MemcacheCacheConfiguration.Server( listOf(MemcacheCacheConfiguration.Server(
@@ -88,10 +86,10 @@ object GraalNativeImageConfiguration {
4) 4)
), ),
Duration.ofSeconds(60), Duration.ofSeconds(60),
"someCustomPrefix",
"MD5", "MD5",
null, null,
1, 1,
0x1000
) )
) )
@@ -102,11 +100,15 @@ object GraalNativeImageConfiguration {
100, 100,
null, null,
Configuration.EventExecutor(true), Configuration.EventExecutor(true),
Configuration.RateLimiter(
false, 0x100000, 10
),
Configuration.Connection( Configuration.Connection(
Duration.ofSeconds(10), Duration.ofSeconds(10),
Duration.ofSeconds(15), Duration.ofSeconds(15),
Duration.ofSeconds(15), Duration.ofSeconds(15),
0x10000, 0x10000,
0x1000
), ),
users.asSequence().map { it.name to it }.toMap(), users.asSequence().map { it.name to it }.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(), sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
@@ -115,27 +117,16 @@ object GraalNativeImageConfiguration {
null, 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 serverHandle = RemoteBuildCacheServer(serverConfiguration).run()
val clientProfile = ClientConfiguration.Profile( val clientProfile = ClientConfiguration.Profile(
URI.create("http://127.0.0.1:$serverPort/"), URI.create("http://127.0.0.1:$serverPort/"),
null, ClientConfiguration.Connection(
Duration.ofSeconds(5),
Duration.ofSeconds(5),
Duration.ofSeconds(7),
true,
),
ClientConfiguration.Authentication.BasicAuthenticationCredentials("user3", PASSWORD), ClientConfiguration.Authentication.BasicAuthenticationCredentials("user3", PASSWORD),
Duration.ofSeconds(3), Duration.ofSeconds(3),
10, 10,
@@ -177,6 +168,8 @@ object GraalNativeImageConfiguration {
} catch (ee : ExecutionException) { } catch (ee : ExecutionException) {
} }
} }
RemoteBuildCacheServerCli.main("--help") System.setProperty("net.woggioni.rbcs.conf.dir", System.getProperty("gradle.tmp.dir"))
RemoteBuildCacheServerCli.createCommandLine().execute("--version")
RemoteBuildCacheServerCli.createCommandLine().execute("server", "-t", "PT10S")
} }
} }

View File

@@ -26,8 +26,8 @@ class RemoteBuildCacheServerCli : RbcsCommand() {
private fun setPropertyIfNotPresent(key: String, value: String) { private fun setPropertyIfNotPresent(key: String, value: String) {
System.getProperty(key) ?: System.setProperty(key, value) System.getProperty(key) ?: System.setProperty(key, value)
} }
@JvmStatic
fun main(vararg args: String) { fun createCommandLine() : CommandLine {
setPropertyIfNotPresent("logback.configurationFile", "net/woggioni/rbcs/cli/logback.xml") setPropertyIfNotPresent("logback.configurationFile", "net/woggioni/rbcs/cli/logback.xml")
setPropertyIfNotPresent("io.netty.leakDetectionLevel", "DISABLED") setPropertyIfNotPresent("io.netty.leakDetectionLevel", "DISABLED")
val currentClassLoader = RemoteBuildCacheServerCli::class.java.classLoader val currentClassLoader = RemoteBuildCacheServerCli::class.java.classLoader
@@ -56,7 +56,12 @@ class RemoteBuildCacheServerCli : RbcsCommand() {
addSubcommand(GetCommand()) addSubcommand(GetCommand())
addSubcommand(HealthCheckCommand()) addSubcommand(HealthCheckCommand())
}) })
System.exit(commandLine.execute(*args)) return commandLine
}
@JvmStatic
fun main(vararg args: String) {
System.exit(createCommandLine().execute(*args))
} }
} }

View File

@@ -1,9 +1,9 @@
package net.woggioni.rbcs.cli.impl package net.woggioni.rbcs.cli.impl
import picocli.CommandLine
import java.util.jar.Attributes import java.util.jar.Attributes
import java.util.jar.JarFile import java.util.jar.JarFile
import java.util.jar.Manifest import java.util.jar.Manifest
import picocli.CommandLine
abstract class AbstractVersionProvider : CommandLine.IVersionProvider { abstract class AbstractVersionProvider : CommandLine.IVersionProvider {

View File

@@ -1,8 +1,8 @@
package net.woggioni.rbcs.cli.impl package net.woggioni.rbcs.cli.impl
import java.nio.file.Path
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import picocli.CommandLine import picocli.CommandLine
import java.nio.file.Path
abstract class RbcsCommand : Runnable { abstract class RbcsCommand : Runnable {
@@ -12,7 +12,7 @@ abstract class RbcsCommand : Runnable {
private set private set
protected fun findConfigurationFile(app: Application, fileName : String): Path { protected fun findConfigurationFile(app: Application, fileName : String): Path {
val confDir = app.computeConfigurationDirectory() val confDir = app.computeConfigurationDirectory(false)
val configurationFile = confDir.resolve(fileName) val configurationFile = confDir.resolve(fileName)
return configurationFile return configurationFile
} }

View File

@@ -1,5 +1,13 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
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
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.LongMath import net.woggioni.jwo.LongMath
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
@@ -12,14 +20,6 @@ import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.error import net.woggioni.rbcs.common.error
import net.woggioni.rbcs.common.info import net.woggioni.rbcs.common.info
import picocli.CommandLine 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( @CommandLine.Command(
name = "benchmark", name = "benchmark",
@@ -101,6 +101,7 @@ class BenchmarkCommand : RbcsCommand() {
"Starting retrieval" "Starting retrieval"
} }
if (entries.isNotEmpty()) { if (entries.isNotEmpty()) {
val errorCounter = AtomicLong(0)
val completionCounter = AtomicLong(0) val completionCounter = AtomicLong(0)
val semaphore = Semaphore(profile.maxConnections * 5) val semaphore = Semaphore(profile.maxConnections * 5)
val start = Instant.now() val start = Instant.now()
@@ -109,14 +110,20 @@ class BenchmarkCommand : RbcsCommand() {
if (it.hasNext()) { if (it.hasNext()) {
val entry = it.next() val entry = it.next()
semaphore.acquire() semaphore.acquire()
val future = client.get(entry.first).thenApply { val future = client.get(entry.first).handle { response, ex ->
if (it == null) { if(ex != null) {
errorCounter.incrementAndGet()
log.error(ex.message, ex)
} else if (response == null) {
errorCounter.incrementAndGet()
log.error { log.error {
"Missing entry for key '${entry.first}'" "Missing entry for key '${entry.first}'"
} }
} else if (!entry.second.contentEquals(it)) { } else if (!entry.second.contentEquals(response)) {
errorCounter.incrementAndGet()
log.error { log.error {
"Retrieved a value different from what was inserted for key '${entry.first}'" "Retrieved a value different from what was inserted for key '${entry.first}': " +
"expected '${JWO.bytesToHex(entry.second)}', got '${JWO.bytesToHex(response)}' instead"
} }
} }
} }
@@ -134,6 +141,12 @@ class BenchmarkCommand : RbcsCommand() {
} }
} }
val end = Instant.now() val end = Instant.now()
val errors = errorCounter.get()
val successfulRetrievals = entries.size - errors
val successRate = successfulRetrievals.toDouble() / entries.size
log.info {
"Successfully retrieved ${entries.size - errors}/${entries.size} (${String.format("%.1f", successRate * 100)}%)"
}
log.info { log.info {
val elapsed = Duration.between(start, end).toMillis() val elapsed = Duration.between(start, end).toMillis()
val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000) val opsPerSecond = String.format("%.2f", entries.size.toDouble() / elapsed * 1000)

View File

@@ -1,13 +1,12 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import java.nio.file.Path
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug import net.woggioni.rbcs.common.debug
import picocli.CommandLine import picocli.CommandLine
import java.lang.IllegalArgumentException
import java.nio.file.Path
@CommandLine.Command( @CommandLine.Command(
name = "client", name = "client",

View File

@@ -1,13 +1,13 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine import picocli.CommandLine
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
@CommandLine.Command( @CommandLine.Command(
name = "get", name = "get",

View File

@@ -1,12 +1,12 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import java.security.SecureRandom
import kotlin.random.Random
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.client.Configuration import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine import picocli.CommandLine
import java.security.SecureRandom
import kotlin.random.Random
@CommandLine.Command( @CommandLine.Command(
name = "health", name = "health",

View File

@@ -1,13 +1,13 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.io.PrintWriter
import net.woggioni.jwo.UncloseableOutputStream import net.woggioni.jwo.UncloseableOutputStream
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
import net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter import net.woggioni.rbcs.cli.impl.converters.OutputStreamConverter
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import picocli.CommandLine import picocli.CommandLine
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.io.PrintWriter
@CommandLine.Command( @CommandLine.Command(

View File

@@ -1,5 +1,9 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
import net.woggioni.jwo.Hash import net.woggioni.jwo.Hash
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.NullOutputStream import net.woggioni.jwo.NullOutputStream
@@ -9,10 +13,6 @@ import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.client.RemoteBuildCacheClient import net.woggioni.rbcs.client.RemoteBuildCacheClient
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import picocli.CommandLine import picocli.CommandLine
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
@CommandLine.Command( @CommandLine.Command(
name = "put", name = "put",

View File

@@ -1,5 +1,10 @@
package net.woggioni.rbcs.cli.impl.commands package net.woggioni.rbcs.cli.impl.commands
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.TimeUnit
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.rbcs.cli.impl.RbcsCommand import net.woggioni.rbcs.cli.impl.RbcsCommand
@@ -10,11 +15,6 @@ import net.woggioni.rbcs.common.info
import net.woggioni.rbcs.server.RemoteBuildCacheServer import net.woggioni.rbcs.server.RemoteBuildCacheServer
import net.woggioni.rbcs.server.RemoteBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL import net.woggioni.rbcs.server.RemoteBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
import picocli.CommandLine import picocli.CommandLine
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.TimeUnit
@CommandLine.Command( @CommandLine.Command(
name = "server", name = "server",

View File

@@ -1,7 +1,7 @@
package net.woggioni.rbcs.cli.impl.converters package net.woggioni.rbcs.cli.impl.converters
import picocli.CommandLine
import java.time.Duration import java.time.Duration
import picocli.CommandLine
class DurationConverter : CommandLine.ITypeConverter<Duration> { class DurationConverter : CommandLine.ITypeConverter<Duration> {

View File

@@ -1,9 +1,9 @@
package net.woggioni.rbcs.cli.impl.converters package net.woggioni.rbcs.cli.impl.converters
import picocli.CommandLine
import java.io.InputStream import java.io.InputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import picocli.CommandLine
class InputStreamConverter : CommandLine.ITypeConverter<InputStream> { class InputStreamConverter : CommandLine.ITypeConverter<InputStream> {

View File

@@ -1,9 +1,9 @@
package net.woggioni.rbcs.cli.impl.converters package net.woggioni.rbcs.cli.impl.converters
import picocli.CommandLine
import java.io.OutputStream import java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import picocli.CommandLine
class OutputStreamConverter : CommandLine.ITypeConverter<OutputStream> { class OutputStreamConverter : CommandLine.ITypeConverter<OutputStream> {

View File

@@ -1,13 +1,13 @@
package net.woggioni.rbcs.client package net.woggioni.rbcs.client
import net.woggioni.rbcs.client.impl.Parser
import net.woggioni.rbcs.common.Xml
import java.net.URI import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.PrivateKey import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Duration import java.time.Duration
import net.woggioni.rbcs.client.impl.Parser
import net.woggioni.rbcs.common.Xml
data class Configuration( data class Configuration(
val profiles: Map<String, Profile> val profiles: Map<String, Profile>
@@ -38,11 +38,12 @@ data class Configuration(
val readIdleTimeout: Duration, val readIdleTimeout: Duration,
val writeIdleTimeout: Duration, val writeIdleTimeout: Duration,
val idleTimeout: Duration, val idleTimeout: Duration,
val requestPipelining : Boolean,
) )
data class Profile( data class Profile(
val serverURI: URI, val serverURI: URI,
val connection: Connection?, val connection: Connection,
val authentication: Authentication?, val authentication: Authentication?,
val connectionTimeout: Duration?, val connectionTimeout: Duration?,
val maxConnections: Int, val maxConnections: Int,

View File

@@ -4,13 +4,13 @@ import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPipeline import io.netty.channel.ChannelPipeline
import io.netty.channel.IoEventLoopGroup
import io.netty.channel.MultiThreadIoEventLoopGroup
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioIoHandler
import io.netty.channel.pool.AbstractChannelPoolHandler import io.netty.channel.pool.AbstractChannelPoolHandler
import io.netty.channel.pool.ChannelPool import io.netty.channel.pool.ChannelPool
import io.netty.channel.pool.FixedChannelPool import io.netty.channel.pool.FixedChannelPool
@@ -34,12 +34,8 @@ import io.netty.handler.timeout.IdleState
import io.netty.handler.timeout.IdleStateEvent import io.netty.handler.timeout.IdleStateEvent
import io.netty.handler.timeout.IdleStateHandler import io.netty.handler.timeout.IdleStateHandler
import io.netty.util.concurrent.Future import io.netty.util.concurrent.Future
import io.netty.util.concurrent.Future as NettyFuture
import io.netty.util.concurrent.GenericFutureListener import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.RBCS.loadKeystore
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.trace
import java.io.IOException import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
@@ -52,19 +48,23 @@ import java.util.concurrent.atomic.AtomicInteger
import javax.net.ssl.TrustManagerFactory import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
import kotlin.random.Random import kotlin.random.Random
import io.netty.util.concurrent.Future as NettyFuture import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.RBCS.loadKeystore
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.trace
class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable { class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoCloseable {
companion object{ companion object {
private val log = createLogger<RemoteBuildCacheClient>() private val log = createLogger<RemoteBuildCacheClient>()
} }
private val group: NioEventLoopGroup private val group: IoEventLoopGroup
private val sslContext: SslContext private val sslContext: SslContext
private val pool: ChannelPool private val pool: ChannelPool
init { init {
group = NioEventLoopGroup() group = MultiThreadIoEventLoopGroup(NioIoHandler.newFactory())
sslContext = SslContextBuilder.forClient().also { builder -> sslContext = SslContextBuilder.forClient().also { builder ->
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials -> (profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
builder.apply { builder.apply {
@@ -73,7 +73,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
*tlsClientAuthenticationCredentials.certificateChain *tlsClientAuthenticationCredentials.certificateChain
) )
profile.tlsTruststore?.let { trustStore -> profile.tlsTruststore?.let { trustStore ->
if(!trustStore.verifyServerCertificate) { if (!trustStore.verifyServerCertificate) {
trustManager(object : X509TrustManager { trustManager(object : X509TrustManager {
override fun checkClientTrusted(certChain: Array<out X509Certificate>, p1: String?) { override fun checkClientTrusted(certChain: Array<out X509Certificate>, p1: String?) {
} }
@@ -152,7 +152,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
} }
val pipeline: ChannelPipeline = ch.pipeline() val pipeline: ChannelPipeline = ch.pipeline()
profile.connection?.also { conn -> profile.connection.also { conn ->
val readIdleTimeout = conn.readIdleTimeout.toMillis() val readIdleTimeout = conn.readIdleTimeout.toMillis()
val writeIdleTimeout = conn.writeIdleTimeout.toMillis() val writeIdleTimeout = conn.writeIdleTimeout.toMillis()
val idleTimeout = conn.idleTimeout.toMillis() val idleTimeout = conn.idleTimeout.toMillis()
@@ -176,7 +176,7 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
// HTTP handlers // HTTP handlers
pipeline.addLast("codec", HttpClientCodec()) pipeline.addLast("codec", HttpClientCodec())
if(profile.compressionEnabled) { if (profile.compressionEnabled) {
pipeline.addLast("decompressor", HttpContentDecompressor()) pipeline.addLast("decompressor", HttpContentDecompressor())
} }
pipeline.addLast("aggregator", HttpObjectAggregator(134217728)) pipeline.addLast("aggregator", HttpObjectAggregator(134217728))
@@ -254,19 +254,25 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
fun get(key: String): CompletableFuture<ByteArray?> { fun get(key: String): CompletableFuture<ByteArray?> {
return executeWithRetry { return executeWithRetry {
sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null) sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)
}.thenApply { }.thenApply { response ->
val status = it.status() val status = response.status()
if (it.status() == HttpResponseStatus.NOT_FOUND) { if (response.status() == HttpResponseStatus.NOT_FOUND) {
response.release()
null null
} else if (it.status() != HttpResponseStatus.OK) { } else if (response.status() != HttpResponseStatus.OK) {
response.release()
throw HttpException(status) throw HttpException(status)
} else { } else {
it.content() response.content().also {
it.retain()
response.release()
}
} }
}.thenApply { maybeByteBuf -> }.thenApply { maybeByteBuf ->
maybeByteBuf?.let { maybeByteBuf?.let { buf ->
val result = ByteArray(it.readableBytes()) val result = ByteArray(buf.readableBytes())
it.getBytes(0, result) buf.getBytes(0, result)
buf.release()
result result
} }
} }
@@ -295,50 +301,33 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
): CompletableFuture<FullHttpResponse> { ): CompletableFuture<FullHttpResponse> {
val responseFuture = CompletableFuture<FullHttpResponse>() val responseFuture = CompletableFuture<FullHttpResponse>()
// Custom handler for processing responses // Custom handler for processing responses
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> { pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
private val handlers = mutableListOf<ChannelHandler>()
fun cleanup(channel: Channel, pipeline: ChannelPipeline) {
handlers.forEach(pipeline::remove)
pool.release(channel)
}
override fun operationComplete(channelFuture: Future<Channel>) { override fun operationComplete(channelFuture: Future<Channel>) {
if (channelFuture.isSuccess) { if (channelFuture.isSuccess) {
val channel = channelFuture.now val channel = channelFuture.now
val pipeline = channel.pipeline() val pipeline = channel.pipeline()
val timeoutHandler = object : ChannelInboundHandlerAdapter() {
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is IdleStateEvent) {
val te = when (evt.state()) {
IdleState.READER_IDLE -> TimeoutException(
"Read timeout",
)
IdleState.WRITER_IDLE -> TimeoutException("Write timeout")
IdleState.ALL_IDLE -> TimeoutException("Idle timeout")
null -> throw IllegalStateException("This should never happen")
}
responseFuture.completeExceptionally(te)
ctx.close()
}
}
}
val closeListener = GenericFutureListener<Future<Void>> { val closeListener = GenericFutureListener<Future<Void>> {
responseFuture.completeExceptionally(IOException("The remote server closed the connection")) responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
pool.release(channel)
} }
channel.closeFuture().addListener(closeListener)
val responseHandler = object : SimpleChannelInboundHandler<FullHttpResponse>() { val responseHandler = object : SimpleChannelInboundHandler<FullHttpResponse>() {
override fun handlerAdded(ctx: ChannelHandlerContext) {
channel.closeFuture().removeListener(closeListener)
}
override fun channelRead0( override fun channelRead0(
ctx: ChannelHandlerContext, ctx: ChannelHandlerContext,
response: FullHttpResponse response: FullHttpResponse
) { ) {
channel.closeFuture().removeListener(closeListener) pipeline.remove(this)
cleanup(channel, pipeline) responseFuture.complete(response.retainedDuplicate())
responseFuture.complete(response) if (!profile.connection.requestPipelining) {
pool.release(channel)
}
} }
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
@@ -352,16 +341,33 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
} }
override fun channelInactive(ctx: ChannelHandlerContext) { override fun channelInactive(ctx: ChannelHandlerContext) {
pool.release(channel)
responseFuture.completeExceptionally(IOException("The remote server closed the connection")) responseFuture.completeExceptionally(IOException("The remote server closed the connection"))
super.channelInactive(ctx) super.channelInactive(ctx)
pool.release(channel)
}
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is IdleStateEvent) {
val te = when (evt.state()) {
IdleState.READER_IDLE -> TimeoutException("Read timeout")
IdleState.WRITER_IDLE -> TimeoutException("Write timeout")
IdleState.ALL_IDLE -> TimeoutException("Idle timeout")
null -> throw IllegalStateException("This should never happen")
}
responseFuture.completeExceptionally(te)
super.userEventTriggered(ctx, evt)
if (this === pipeline.last()) {
ctx.close()
}
if (!profile.connection.requestPipelining) {
pool.release(channel)
}
} else {
super.userEventTriggered(ctx, evt)
}
} }
} }
for (handler in arrayOf(timeoutHandler, responseHandler)) { pipeline.addLast(responseHandler)
handlers.add(handler)
}
pipeline.addLast(timeoutHandler, responseHandler)
channel.closeFuture().addListener(closeListener)
// Prepare the HTTP request // Prepare the HTTP request
@@ -373,13 +379,14 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
uri.rawPath, uri.rawPath,
content ?: Unpooled.buffer(0) content ?: Unpooled.buffer(0)
).apply { ).apply {
// Set headers
headers().apply { headers().apply {
if (content != null) { if (content != null) {
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()) set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
} }
set(HttpHeaderNames.HOST, profile.serverURI.host) set(HttpHeaderNames.HOST, profile.serverURI.host)
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE) set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
if(profile.compressionEnabled) { if (profile.compressionEnabled) {
set( set(
HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString() HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
@@ -398,9 +405,16 @@ class RemoteBuildCacheClient(private val profile: Configuration.Profile) : AutoC
} }
} }
// Set headers
// Send the request // Send the request
channel.writeAndFlush(request) channel.writeAndFlush(request).addListener {
if (!it.isSuccess) {
val ex = it.cause()
log.warn(ex.message, ex)
}
if (profile.connection.requestPipelining) {
pool.release(channel)
}
}
} else { } else {
responseFuture.completeExceptionally(channelFuture.cause()) responseFuture.completeExceptionally(channelFuture.cause())
} }

View File

@@ -1,10 +1,5 @@
package net.woggioni.rbcs.client.impl package net.woggioni.rbcs.client.impl
import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import java.net.URI import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@@ -13,6 +8,11 @@ import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Duration import java.time.Duration
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.client.Configuration
import net.woggioni.rbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document
object Parser { object Parser {
@@ -30,7 +30,12 @@ object Parser {
?: throw ConfigurationException("base-url attribute is required") ?: throw ConfigurationException("base-url attribute is required")
var authentication: Configuration.Authentication? = null var authentication: Configuration.Authentication? = null
var retryPolicy: Configuration.RetryPolicy? = null var retryPolicy: Configuration.RetryPolicy? = null
var connection : Configuration.Connection? = null var connection : Configuration.Connection = Configuration.Connection(
Duration.ofSeconds(60),
Duration.ofSeconds(60),
Duration.ofSeconds(30),
false
)
var trustStore : Configuration.TrustStore? = null var trustStore : Configuration.TrustStore? = null
for (gchild in child.asIterable()) { for (gchild in child.asIterable()) {
when (gchild.localName) { when (gchild.localName) {
@@ -97,10 +102,13 @@ object Parser {
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS) ?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout") val writeIdleTimeout = gchild.renderAttribute("write-idle-timeout")
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS) ?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val requestPipelining = gchild.renderAttribute("request-pipelining")
?.let(String::toBoolean) ?: false
connection = Configuration.Connection( connection = Configuration.Connection(
readIdleTimeout, readIdleTimeout,
writeIdleTimeout, writeIdleTimeout,
idleTimeout, idleTimeout,
requestPipelining
) )
} }

View File

@@ -123,6 +123,13 @@
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
</xs:attribute> </xs:attribute>
<xs:attribute name="request-pipelining" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>
Enables HTTP/1.1 request pipelining
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="noAuthType"> <xs:complexType name="noAuthType">

View File

@@ -2,6 +2,9 @@ package net.woggioni.rbcs.client
import io.netty.util.concurrent.DefaultEventExecutorGroup import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup import io.netty.util.concurrent.EventExecutorGroup
import java.util.concurrent.CompletableFuture
import java.util.stream.Stream
import kotlin.random.Random
import net.woggioni.rbcs.common.contextLogger import net.woggioni.rbcs.common.contextLogger
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.ExtensionContext
@@ -9,9 +12,7 @@ import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider import org.junit.jupiter.params.provider.ArgumentsProvider
import org.junit.jupiter.params.provider.ArgumentsSource import org.junit.jupiter.params.provider.ArgumentsSource
import java.util.concurrent.CompletableFuture import org.junit.jupiter.params.support.ParameterDeclarations
import java.util.stream.Stream
import kotlin.random.Random
class RetryTest { class RetryTest {
@@ -23,7 +24,10 @@ class RetryTest {
) )
class TestArguments : ArgumentsProvider { class TestArguments : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext): Stream<out Arguments> { override fun provideArguments(
parameters: ParameterDeclarations,
context: ExtensionContext
): Stream<out Arguments> {
return Stream.of( return Stream.of(
TestArgs( TestArgs(
seed = 101325, seed = 101325,

View File

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

View File

@@ -2,14 +2,14 @@ package net.woggioni.rbcs.common
import io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.slf4j.MDC import org.slf4j.MDC
import org.slf4j.event.Level import org.slf4j.event.Level
import org.slf4j.spi.LoggingEventBuilder import org.slf4j.spi.LoggingEventBuilder
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java) inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun <reified T> createLogger() = LoggerFactory.getLogger(T::class.java) inline fun <reified T> createLogger() = LoggerFactory.getLogger(T::class.java)

View File

@@ -1,7 +1,5 @@
package net.woggioni.rbcs.common package net.woggioni.rbcs.common
import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2
import java.io.IOException import java.io.IOException
import java.net.InetAddress import java.net.InetAddress
import java.net.ServerSocket import java.net.ServerSocket
@@ -21,26 +19,28 @@ import java.security.cert.X509Certificate
import java.util.EnumSet import java.util.EnumSet
import javax.net.ssl.TrustManagerFactory import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2
object RBCS { object RBCS {
fun String.toUrl() : URL = URL.of(URI(this), null) fun String.toUrl(): URL = URL.of(URI(this), null)
const val RBCS_NAMESPACE_URI: String = "urn:net.woggioni.rbcs.server" const val RBCS_NAMESPACE_URI: String = "urn:net.woggioni.rbcs.server"
const val RBCS_PREFIX: String = "rbcs" const val RBCS_PREFIX: String = "rbcs"
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance" const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
fun ByteArray.toInt(index : Int = 0) : Long { fun ByteArray.toInt(index: Int = 0): Long {
if(index + 4 > size) throw IllegalArgumentException("Not enough bytes to decode a 32 bits integer") if (index + 4 > size) throw IllegalArgumentException("Not enough bytes to decode a 32 bits integer")
var value : Long = 0 var value: Long = 0
for (b in index until index + 4) { for (b in index until index + 4) {
value = (value shl 8) + (get(b).toInt() and 0xFF) value = (value shl 8) + (get(b).toInt() and 0xFF)
} }
return value return value
} }
fun ByteArray.toLong(index : Int = 0) : Long { fun ByteArray.toLong(index: Int = 0): Long {
if(index + 8 > size) throw IllegalArgumentException("Not enough bytes to decode a 64 bits long integer") if (index + 8 > size) throw IllegalArgumentException("Not enough bytes to decode a 64 bits long integer")
var value : Long = 0 var value: Long = 0
for (b in index until index + 8) { for (b in index until index + 8) {
value = (value shl 8) + (get(b).toInt() and 0xFF) value = (value shl 8) + (get(b).toInt() and 0xFF)
} }
@@ -62,11 +62,18 @@ object RBCS {
return JWO.bytesToHex(digest(data, md)) return JWO.bytesToHex(digest(data, md))
} }
fun processCacheKey(key: String, digestAlgorithm: String?) = digestAlgorithm fun processCacheKey(key: String, keyPrefix: String?, digestAlgorithm: String?) : ByteArray {
?.let(MessageDigest::getInstance) val prefixedKey = if (keyPrefix == null) {
?.let { md -> key
digest(key.toByteArray(), md) } else {
} ?: key.toByteArray(Charsets.UTF_8) key + keyPrefix
}.toByteArray(Charsets.UTF_8)
return digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digest(prefixedKey, md)
} ?: prefixedKey
}
fun Long.toIntOrNull(): Int? { fun Long.toIntOrNull(): Int? {
return if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) { return if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) {

View File

@@ -1,14 +1,5 @@
package net.woggioni.rbcs.common package net.woggioni.rbcs.common
import net.woggioni.jwo.JWO
import org.slf4j.event.Level
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.SAXNotRecognizedException
import org.xml.sax.SAXNotSupportedException
import org.xml.sax.SAXParseException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.net.URL import java.net.URL
@@ -25,7 +16,16 @@ import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource import javax.xml.transform.stream.StreamSource
import javax.xml.validation.Schema import javax.xml.validation.Schema
import javax.xml.validation.SchemaFactory import javax.xml.validation.SchemaFactory
import net.woggioni.jwo.JWO
import org.slf4j.event.Level
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.ErrorHandler as ErrHandler import org.xml.sax.ErrorHandler as ErrHandler
import org.xml.sax.SAXNotRecognizedException
import org.xml.sax.SAXNotSupportedException
import org.xml.sax.SAXParseException
class NodeListIterator(private val nodeList: NodeList) : Iterator<Node> { class NodeListIterator(private val nodeList: NodeList) : Iterator<Node> {

View File

@@ -1,14 +1,14 @@
package net.woggioni.rbcs.common package net.woggioni.rbcs.common
import java.security.Provider
import java.security.Security
import java.util.Base64
import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash import net.woggioni.rbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.EnumSource
import java.security.Provider
import java.security.Security
import java.util.Base64
class PasswordHashingTest { class PasswordHashingTest {

View File

@@ -19,10 +19,12 @@ to `memcacheCacheType`.
The plugins currently supports the following configuration attributes: The plugins currently supports the following configuration attributes:
- `max-age`: the amount of time cache entries will be retained on memcache - `max-age`: the amount of time cache entries will be retained on memcache
- `key-prefix`: a string that will be prepended to all the keys inserted in memcache,
useful in case the caching backend is shared with other applications
- `digest`: digest algorithm to use on the key before submission - `digest`: digest algorithm to use on the key before submission
to memcache (optional, no digest is applied if omitted) to memcache (optional, no digest is applied if omitted)
- `compression`: compression algorithm to apply to cache values before, - `compression`: compression algorithm to apply to cache values before,
currently only `deflate` is supported (optionla, if omitted compression is disabled) currently only `deflate` is supported (optional, if omitted compression is disabled)
- `compression-level`: compression level to use, deflate supports compression levels from 1 to 9, - `compression-level`: compression level to use, deflate supports compression levels from 1 to 9,
where 1 is for fast compression at the expense of speed (optional, 6 is used if omitted) where 1 is for fast compression at the expense of speed (optional, 6 is used if omitted)
```xml ```xml
@@ -35,10 +37,10 @@ The plugins currently supports the following configuration attributes:
... ...
<cache xs:type="rbcs-memcache:memcacheCacheType" <cache xs:type="rbcs-memcache:memcacheCacheType"
max-age="P7D" max-age="P7D"
key-prefix="rbcs-"
digest="SHA-256" digest="SHA-256"
compression-mode="deflate" compression-mode="deflate"
compression-level="6" compression-level="6">
chunk-size="0x10000">
<server host="127.0.0.1" port="11211" max-connections="256"/> <server host="127.0.0.1" port="11211" max-connections="256"/>
<server host="127.0.0.1" port="11212" max-connections="256"/> <server host="127.0.0.1" port="11212" max-connections="256"/>
</cache> </cache>

View File

@@ -1,29 +1,29 @@
package net.woggioni.rbcs.server.memcache package net.woggioni.rbcs.server.memcache
import io.netty.channel.ChannelFactory import io.netty.channel.ChannelFactory
import io.netty.channel.ChannelHandler
import io.netty.channel.EventLoopGroup import io.netty.channel.EventLoopGroup
import io.netty.channel.pool.FixedChannelPool import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.DatagramChannel import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.SocketChannel
import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.server.memcache.client.MemcacheClient
import java.time.Duration import java.time.Duration
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import net.woggioni.rbcs.api.CacheHandler
import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.server.memcache.client.MemcacheClient
data class MemcacheCacheConfiguration( data class MemcacheCacheConfiguration(
val servers: List<Server>, val servers: List<Server>,
val maxAge: Duration = Duration.ofDays(1), val maxAge: Duration = Duration.ofDays(1),
val keyPrefix : String? = null,
val digestAlgorithm: String? = null, val digestAlgorithm: String? = null,
val compressionMode: CompressionMode? = null, val compressionMode: CompressionMode? = null,
val compressionLevel: Int, val compressionLevel: Int,
val chunkSize: Int
) : Configuration.Cache { ) : Configuration.Cache {
companion object { companion object {
@@ -48,22 +48,24 @@ data class MemcacheCacheConfiguration(
private val connectionPoolMap = ConcurrentHashMap<HostAndPort, FixedChannelPool>() private val connectionPoolMap = ConcurrentHashMap<HostAndPort, FixedChannelPool>()
override fun newHandler( override fun newHandler(
cfg : Configuration,
eventLoop: EventLoopGroup, eventLoop: EventLoopGroup,
socketChannelFactory: ChannelFactory<SocketChannel>, socketChannelFactory: ChannelFactory<SocketChannel>,
datagramChannelFactory: ChannelFactory<DatagramChannel> datagramChannelFactory: ChannelFactory<DatagramChannel>,
): ChannelHandler { ): CacheHandler {
return MemcacheCacheHandler( return MemcacheCacheHandler(
MemcacheClient( MemcacheClient(
this@MemcacheCacheConfiguration.servers, this@MemcacheCacheConfiguration.servers,
chunkSize, cfg.connection.chunkSize,
eventLoop, eventLoop,
socketChannelFactory, socketChannelFactory,
connectionPoolMap connectionPoolMap
), ),
keyPrefix,
digestAlgorithm, digestAlgorithm,
compressionMode != null, compressionMode != null,
compressionLevel, compressionLevel,
chunkSize, cfg.connection.chunkSize,
maxAge maxAge
) )
} }

View File

@@ -3,8 +3,8 @@ package net.woggioni.rbcs.server.memcache
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.CompositeByteBuf import io.netty.buffer.CompositeByteBuf
import io.netty.channel.Channel as NettyChannel
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.memcache.DefaultLastMemcacheContent import io.netty.handler.codec.memcache.DefaultLastMemcacheContent
import io.netty.handler.codec.memcache.DefaultMemcacheContent import io.netty.handler.codec.memcache.DefaultMemcacheContent
import io.netty.handler.codec.memcache.LastMemcacheContent import io.netty.handler.codec.memcache.LastMemcacheContent
@@ -13,6 +13,22 @@ import io.netty.handler.codec.memcache.binary.BinaryMemcacheOpcodes
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponseStatus import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponseStatus
import io.netty.handler.codec.memcache.binary.DefaultBinaryMemcacheRequest import io.netty.handler.codec.memcache.binary.DefaultBinaryMemcacheRequest
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.nio.channels.FileChannel
import java.nio.channels.ReadableByteChannel
import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterOutputStream
import net.woggioni.rbcs.api.CacheHandler
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.api.exception.ContentTooLargeException import net.woggioni.rbcs.api.exception.ContentTooLargeException
import net.woggioni.rbcs.api.message.CacheMessage import net.woggioni.rbcs.api.message.CacheMessage
@@ -34,31 +50,16 @@ import net.woggioni.rbcs.common.trace
import net.woggioni.rbcs.server.memcache.client.MemcacheClient import net.woggioni.rbcs.server.memcache.client.MemcacheClient
import net.woggioni.rbcs.server.memcache.client.MemcacheRequestController import net.woggioni.rbcs.server.memcache.client.MemcacheRequestController
import net.woggioni.rbcs.server.memcache.client.MemcacheResponseHandler import net.woggioni.rbcs.server.memcache.client.MemcacheResponseHandler
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.nio.channels.FileChannel
import java.nio.channels.ReadableByteChannel
import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterOutputStream
import io.netty.channel.Channel as NettyChannel
class MemcacheCacheHandler( class MemcacheCacheHandler(
private val client: MemcacheClient, private val client: MemcacheClient,
private val keyPrefix: String?,
private val digestAlgorithm: String?, private val digestAlgorithm: String?,
private val compressionEnabled: Boolean, private val compressionEnabled: Boolean,
private val compressionLevel: Int, private val compressionLevel: Int,
private val chunkSize: Int, private val chunkSize: Int,
private val maxAge: Duration private val maxAge: Duration
) : SimpleChannelInboundHandler<CacheMessage>() { ) : CacheHandler() {
companion object { companion object {
private val log = createLogger<MemcacheCacheHandler>() private val log = createLogger<MemcacheCacheHandler>()
@@ -69,10 +70,14 @@ class MemcacheCacheHandler(
} }
} }
private interface InProgressRequest {
}
private inner class InProgressGetRequest( private inner class InProgressGetRequest(
private val key: String, val key: String,
private val ctx: ChannelHandlerContext private val ctx: ChannelHandlerContext
) { ) : InProgressRequest {
private val acc = ctx.alloc().compositeBuffer() private val acc = ctx.alloc().compositeBuffer()
private val chunk = ctx.alloc().compositeBuffer() private val chunk = ctx.alloc().compositeBuffer()
private val outputStream = ByteBufOutputStream(chunk).let { private val outputStream = ByteBufOutputStream(chunk).let {
@@ -98,32 +103,35 @@ class MemcacheCacheHandler(
acc.retain() acc.retain()
it.readObject() as CacheValueMetadata it.readObject() as CacheValueMetadata
} }
ctx.writeAndFlush(CacheValueFoundResponse(key, metadata)) log.trace(ctx) {
"Sending response from cache"
}
sendMessageAndFlush(ctx, CacheValueFoundResponse(key, metadata))
responseSent = true responseSent = true
acc.readerIndex(Int.SIZE_BYTES + mSize) acc.readerIndex(Int.SIZE_BYTES + mSize)
} }
if (responseSent) { if (responseSent) {
acc.readBytes(outputStream, acc.readableBytes()) acc.readBytes(outputStream, acc.readableBytes())
if(acc.readableBytes() >= chunkSize) { if (acc.readableBytes() >= chunkSize) {
flush(false) flush(false)
} }
} }
} }
private fun flush(last : Boolean) { private fun flush(last: Boolean) {
val toSend = extractChunk(chunk, ctx.alloc()) val toSend = extractChunk(chunk, ctx.alloc())
val msg = if(last) { val msg = if (last) {
log.trace(ctx) { log.trace(ctx) {
"Sending last chunk to client on channel ${ctx.channel().id().asShortText()}" "Sending last chunk to client"
} }
LastCacheContent(toSend) LastCacheContent(toSend)
} else { } else {
log.trace(ctx) { log.trace(ctx) {
"Sending chunk to client on channel ${ctx.channel().id().asShortText()}" "Sending chunk to client"
} }
CacheContent(toSend) CacheContent(toSend)
} }
ctx.writeAndFlush(msg) sendMessageAndFlush(ctx, msg)
} }
fun commit() { fun commit() {
@@ -141,14 +149,14 @@ class MemcacheCacheHandler(
} }
private inner class InProgressPutRequest( private inner class InProgressPutRequest(
private val ch : NettyChannel, private val ch: NettyChannel,
metadata : CacheValueMetadata, metadata: CacheValueMetadata,
val digest : ByteBuf, val digest: ByteBuf,
val requestController: CompletableFuture<MemcacheRequestController>, val requestController: CompletableFuture<MemcacheRequestController>,
private val alloc: ByteBufAllocator private val alloc: ByteBufAllocator
) { ) : InProgressRequest {
private var totalSize = 0 private var totalSize = 0
private var tmpFile : FileChannel? = null private var tmpFile: FileChannel? = null
private val accumulator = alloc.compositeBuffer() private val accumulator = alloc.compositeBuffer()
private val stream = ByteBufOutputStream(accumulator).let { private val stream = ByteBufOutputStream(accumulator).let {
if (compressionEnabled) { if (compressionEnabled) {
@@ -175,7 +183,7 @@ class MemcacheCacheHandler(
tmpFile?.let { tmpFile?.let {
flushToDisk(it, accumulator) flushToDisk(it, accumulator)
} }
if(accumulator.readableBytes() > 0x100000) { if (accumulator.readableBytes() > 0x100000) {
log.debug(ch) { log.debug(ch) {
"Entry is too big, buffering it into a file" "Entry is too big, buffering it into a file"
} }
@@ -192,18 +200,18 @@ class MemcacheCacheHandler(
} }
} }
private fun flushToDisk(fc : FileChannel, buf : CompositeByteBuf) { private fun flushToDisk(fc: FileChannel, buf: CompositeByteBuf) {
val chunk = extractChunk(buf, alloc) val chunk = extractChunk(buf, alloc)
fc.write(chunk.nioBuffer()) fc.write(chunk.nioBuffer())
chunk.release() chunk.release()
} }
fun commit() : Pair<Int, ReadableByteChannel> { fun commit(): Pair<Int, ReadableByteChannel> {
digest.release() digest.release()
accumulator.retain() accumulator.retain()
stream.close() stream.close()
val fileChannel = tmpFile val fileChannel = tmpFile
return if(fileChannel != null) { return if (fileChannel != null) {
flushToDisk(fileChannel, accumulator) flushToDisk(fileChannel, accumulator)
accumulator.release() accumulator.release()
fileChannel.position(0) fileChannel.position(0)
@@ -224,8 +232,7 @@ class MemcacheCacheHandler(
} }
} }
private var inProgressPutRequest: InProgressPutRequest? = null private var inProgressRequest: InProgressRequest? = null
private var inProgressGetRequest: InProgressGetRequest? = null
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) { override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
when (msg) { when (msg) {
@@ -242,7 +249,7 @@ class MemcacheCacheHandler(
"Fetching ${msg.key} from memcache" "Fetching ${msg.key} from memcache"
} }
val key = ctx.alloc().buffer().also { val key = ctx.alloc().buffer().also {
it.writeBytes(processCacheKey(msg.key, digestAlgorithm)) it.writeBytes(processCacheKey(msg.key, keyPrefix, digestAlgorithm))
} }
val responseHandler = object : MemcacheResponseHandler { val responseHandler = object : MemcacheResponseHandler {
override fun responseReceived(response: BinaryMemcacheResponse) { override fun responseReceived(response: BinaryMemcacheResponse) {
@@ -252,32 +259,39 @@ class MemcacheCacheHandler(
log.debug(ctx) { log.debug(ctx) {
"Cache hit for key ${msg.key} on memcache" "Cache hit for key ${msg.key} on memcache"
} }
inProgressGetRequest = InProgressGetRequest(msg.key, ctx) inProgressRequest = InProgressGetRequest(msg.key, ctx)
} }
BinaryMemcacheResponseStatus.KEY_ENOENT -> { BinaryMemcacheResponseStatus.KEY_ENOENT -> {
log.debug(ctx) { log.debug(ctx) {
"Cache miss for key ${msg.key} on memcache" "Cache miss for key ${msg.key} on memcache"
} }
ctx.writeAndFlush(CacheValueNotFoundResponse()) sendMessageAndFlush(ctx, CacheValueNotFoundResponse(msg.key))
} }
} }
} }
override fun contentReceived(content: MemcacheContent) { override fun contentReceived(content: MemcacheContent) {
log.trace(ctx) { log.trace(ctx) {
"${if(content is LastMemcacheContent) "Last chunk" else "Chunk"} of ${content.content().readableBytes()} bytes received from memcache for key ${msg.key}" "${if (content is LastMemcacheContent) "Last chunk" else "Chunk"} of ${
content.content().readableBytes()
} bytes received from memcache for key ${msg.key}"
} }
inProgressGetRequest?.write(content.content()) (inProgressRequest as? InProgressGetRequest)?.let { inProgressGetRequest ->
if (content is LastMemcacheContent) { inProgressGetRequest.write(content.content())
inProgressGetRequest?.commit() if (content is LastMemcacheContent) {
inProgressRequest = null
inProgressGetRequest.commit()
}
} }
} }
override fun exceptionCaught(ex: Throwable) { override fun exceptionCaught(ex: Throwable) {
inProgressGetRequest?.let { (inProgressRequest as? InProgressGetRequest).let { inProgressGetRequest ->
inProgressGetRequest = null inProgressGetRequest?.let {
it.rollback() inProgressRequest = null
it.rollback()
}
} }
this@MemcacheCacheHandler.exceptionCaught(ctx, ex) this@MemcacheCacheHandler.exceptionCaught(ctx, ex)
} }
@@ -290,12 +304,13 @@ class MemcacheCacheHandler(
setOpcode(BinaryMemcacheOpcodes.GET) setOpcode(BinaryMemcacheOpcodes.GET)
} }
requestHandle.sendRequest(request) requestHandle.sendRequest(request)
requestHandle.sendContent(LastMemcacheContent.EMPTY_LAST_CONTENT)
} }
} }
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) { private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
val key = ctx.alloc().buffer().also { val key = ctx.alloc().buffer().also {
it.writeBytes(processCacheKey(msg.key, digestAlgorithm)) it.writeBytes(processCacheKey(msg.key, keyPrefix, digestAlgorithm))
} }
val responseHandler = object : MemcacheResponseHandler { val responseHandler = object : MemcacheResponseHandler {
override fun responseReceived(response: BinaryMemcacheResponse) { override fun responseReceived(response: BinaryMemcacheResponse) {
@@ -305,8 +320,9 @@ class MemcacheCacheHandler(
log.debug(ctx) { log.debug(ctx) {
"Inserted key ${msg.key} into memcache" "Inserted key ${msg.key} into memcache"
} }
ctx.writeAndFlush(CachePutResponse(msg.key)) sendMessageAndFlush(ctx, CachePutResponse(msg.key))
} }
else -> this@MemcacheCacheHandler.exceptionCaught(ctx, MemcacheException(status)) else -> this@MemcacheCacheHandler.exceptionCaught(ctx, MemcacheException(status))
} }
} }
@@ -323,86 +339,103 @@ class MemcacheCacheHandler(
this@MemcacheCacheHandler.exceptionCaught(ctx, ex) this@MemcacheCacheHandler.exceptionCaught(ctx, ex)
} }
} }
inProgressPutRequest = InProgressPutRequest(ctx.channel(), msg.metadata, key, requestController, ctx.alloc()) inProgressRequest = InProgressPutRequest(ctx.channel(), msg.metadata, key, requestController, ctx.alloc())
} }
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) { private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
inProgressPutRequest?.let { request -> val request = inProgressRequest
log.trace(ctx) { when (request) {
"Received chunk of ${msg.content().readableBytes()} bytes for memcache" is InProgressPutRequest -> {
log.trace(ctx) {
"Received chunk of ${msg.content().readableBytes()} bytes for memcache"
}
request.write(msg.content())
}
is InProgressGetRequest -> {
msg.release()
} }
request.write(msg.content())
} }
} }
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) { private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
inProgressPutRequest?.let { request -> val request = inProgressRequest
inProgressPutRequest = null when (request) {
log.trace(ctx) { is InProgressPutRequest -> {
"Received last chunk of ${msg.content().readableBytes()} bytes for memcache" inProgressRequest = null
} log.trace(ctx) {
request.write(msg.content()) "Received last chunk of ${msg.content().readableBytes()} bytes for memcache"
val key = request.digest.retainedDuplicate() }
val (payloadSize, payloadSource) = request.commit() request.write(msg.content())
val extras = ctx.alloc().buffer(8, 8) val key = request.digest.retainedDuplicate()
extras.writeInt(0) val (payloadSize, payloadSource) = request.commit()
extras.writeInt(encodeExpiry(maxAge)) val extras = ctx.alloc().buffer(8, 8)
val totalBodyLength = request.digest.readableBytes() + extras.readableBytes() + payloadSize extras.writeInt(0)
request.requestController.whenComplete { requestController, ex -> extras.writeInt(encodeExpiry(maxAge))
if(ex == null) { val totalBodyLength = request.digest.readableBytes() + extras.readableBytes() + payloadSize
log.trace(ctx) { log.trace(ctx) {
"Sending SET request to memcache" "Trying to send SET request to memcache"
} }
requestController.sendRequest(DefaultBinaryMemcacheRequest().apply { request.requestController.whenComplete { requestController, ex ->
setOpcode(BinaryMemcacheOpcodes.SET) if (ex == null) {
setKey(key) log.trace(ctx) {
setExtras(extras) "Sending SET request to memcache"
setTotalBodyLength(totalBodyLength) }
}) requestController.sendRequest(DefaultBinaryMemcacheRequest().apply {
log.trace(ctx) { setOpcode(BinaryMemcacheOpcodes.SET)
"Sending request payload to memcache" setKey(key)
} setExtras(extras)
payloadSource.use { source -> setTotalBodyLength(totalBodyLength)
val bb = ByteBuffer.allocate(chunkSize) })
while (true) { log.trace(ctx) {
val read = source.read(bb) "Sending request payload to memcache"
bb.limit() }
if(read >= 0 && bb.position() < chunkSize && bb.hasRemaining()) { payloadSource.use { source ->
continue val bb = ByteBuffer.allocate(chunkSize)
} while (true) {
val chunk = ctx.alloc().buffer(chunkSize) val read = source.read(bb)
bb.flip() bb.limit()
chunk.writeBytes(bb) if (read >= 0 && bb.position() < chunkSize && bb.hasRemaining()) {
bb.clear() continue
log.trace(ctx) { }
"Sending ${chunk.readableBytes()} bytes chunk to memcache" val chunk = ctx.alloc().buffer(chunkSize)
} bb.flip()
if(read < 0) { chunk.writeBytes(bb)
requestController.sendContent(DefaultLastMemcacheContent(chunk)) bb.clear()
break log.trace(ctx) {
} else { "Sending ${chunk.readableBytes()} bytes chunk to memcache"
requestController.sendContent(DefaultMemcacheContent(chunk)) }
if (read < 0) {
requestController.sendContent(DefaultLastMemcacheContent(chunk))
break
} else {
requestController.sendContent(DefaultMemcacheContent(chunk))
}
} }
} }
} else {
payloadSource.close()
} }
} else {
payloadSource.close()
} }
} }
} }
} }
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
inProgressGetRequest?.let { val request = inProgressRequest
inProgressGetRequest = null when (request) {
it.rollback() is InProgressPutRequest -> {
} inProgressRequest = null
inProgressPutRequest?.let { request.requestController.thenAccept { controller ->
inProgressPutRequest = null controller.exceptionCaught(cause)
it.requestController.thenAccept { controller -> }
controller.exceptionCaught(cause) request.rollback()
}
is InProgressGetRequest -> {
inProgressRequest = null
request.rollback()
} }
it.rollback()
} }
super.exceptionCaught(ctx, cause) super.exceptionCaught(ctx, cause)
} }

View File

@@ -1,5 +1,7 @@
package net.woggioni.rbcs.server.memcache package net.woggioni.rbcs.server.memcache
import java.time.Duration
import java.time.temporal.ChronoUnit
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.rbcs.api.CacheProvider
import net.woggioni.rbcs.api.exception.ConfigurationException import net.woggioni.rbcs.api.exception.ConfigurationException
import net.woggioni.rbcs.common.HostAndPort import net.woggioni.rbcs.common.HostAndPort
@@ -9,8 +11,6 @@ import net.woggioni.rbcs.common.Xml.Companion.asIterable
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import java.time.Duration
import java.time.temporal.ChronoUnit
class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> { class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
@@ -28,9 +28,6 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
val maxAge = el.renderAttribute("max-age") val maxAge = el.renderAttribute("max-age")
?.let(Duration::parse) ?.let(Duration::parse)
?: Duration.ofDays(1) ?: Duration.ofDays(1)
val chunkSize = el.renderAttribute("chunk-size")
?.let(Integer::decode)
?: 0x10000
val compressionLevel = el.renderAttribute("compression-level") val compressionLevel = el.renderAttribute("compression-level")
?.let(Integer::decode) ?.let(Integer::decode)
?: -1 ?: -1
@@ -41,6 +38,7 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
else -> MemcacheCacheConfiguration.CompressionMode.DEFLATE else -> MemcacheCacheConfiguration.CompressionMode.DEFLATE
} }
} }
val keyPrefix = el.renderAttribute("key-prefix")
val digestAlgorithm = el.renderAttribute("digest") val digestAlgorithm = el.renderAttribute("digest")
for (child in el.asIterable()) { for (child in el.asIterable()) {
when (child.nodeName) { when (child.nodeName) {
@@ -57,14 +55,13 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
} }
} }
} }
return MemcacheCacheConfiguration( return MemcacheCacheConfiguration(
servers, servers,
maxAge, maxAge,
keyPrefix,
digestAlgorithm, digestAlgorithm,
compressionMode, compressionMode,
compressionLevel, compressionLevel
chunkSize
) )
} }
@@ -82,9 +79,12 @@ class MemcacheCacheProvider : CacheProvider<MemcacheCacheConfiguration> {
} }
attr("max-connections", server.maxConnections.toString()) attr("max-connections", server.maxConnections.toString())
} }
} }
attr("max-age", maxAge.toString()) attr("max-age", maxAge.toString())
attr("chunk-size", chunkSize.toString()) keyPrefix?.let {
attr("key-prefix", it)
}
digestAlgorithm?.let { digestAlgorithm -> digestAlgorithm?.let { digestAlgorithm ->
attr("digest", digestAlgorithm) attr("digest", digestAlgorithm)
} }

View File

@@ -12,7 +12,6 @@ import io.netty.channel.ChannelPipeline
import io.netty.channel.EventLoopGroup import io.netty.channel.EventLoopGroup
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.pool.AbstractChannelPoolHandler import io.netty.channel.pool.AbstractChannelPoolHandler
import io.netty.channel.pool.ChannelPool
import io.netty.channel.pool.FixedChannelPool import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.SocketChannel
import io.netty.handler.codec.memcache.LastMemcacheContent import io.netty.handler.codec.memcache.LastMemcacheContent
@@ -21,17 +20,17 @@ import io.netty.handler.codec.memcache.MemcacheObject
import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec
import io.netty.handler.codec.memcache.binary.BinaryMemcacheRequest import io.netty.handler.codec.memcache.binary.BinaryMemcacheRequest
import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse import io.netty.handler.codec.memcache.binary.BinaryMemcacheResponse
import io.netty.util.concurrent.Future as NettyFuture
import io.netty.util.concurrent.GenericFutureListener import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.warn
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
import net.woggioni.rbcs.server.memcache.MemcacheCacheHandler
import java.io.IOException import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import io.netty.util.concurrent.Future as NettyFuture import net.woggioni.rbcs.common.HostAndPort
import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.trace
import net.woggioni.rbcs.server.memcache.MemcacheCacheConfiguration
import net.woggioni.rbcs.server.memcache.MemcacheCacheHandler
class MemcacheClient( class MemcacheClient(
@@ -94,18 +93,6 @@ class MemcacheClient(
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> { pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
override fun operationComplete(channelFuture: NettyFuture<Channel>) { override fun operationComplete(channelFuture: NettyFuture<Channel>) {
if (channelFuture.isSuccess) { if (channelFuture.isSuccess) {
var requestSent = false
var requestBodySent = false
var requestFinished = false
var responseReceived = false
var responseBodyReceived = false
var responseFinished = false
var requestBodySize = 0
var requestBodyBytesSent = 0
val channel = channelFuture.now val channel = channelFuture.now
var connectionClosedByTheRemoteServer = true var connectionClosedByTheRemoteServer = true
val closeCallback = { val closeCallback = {
@@ -113,14 +100,7 @@ class MemcacheClient(
val ex = IOException("The memcache server closed the connection") val ex = IOException("The memcache server closed the connection")
val completed = response.completeExceptionally(ex) val completed = response.completeExceptionally(ex)
if(!completed) responseHandler.exceptionCaught(ex) if(!completed) responseHandler.exceptionCaught(ex)
log.warn {
"RequestSent: $requestSent, RequestBodySent: $requestBodySent, " +
"RequestFinished: $requestFinished, ResponseReceived: $responseReceived, " +
"ResponseBodyReceived: $responseBodyReceived, ResponseFinished: $responseFinished, " +
"RequestBodySize: $requestBodySize, RequestBodyBytesSent: $requestBodyBytesSent"
}
} }
pool.release(channel)
} }
val closeListener = ChannelFutureListener { val closeListener = ChannelFutureListener {
closeCallback() closeCallback()
@@ -140,18 +120,14 @@ class MemcacheClient(
when (msg) { when (msg) {
is BinaryMemcacheResponse -> { is BinaryMemcacheResponse -> {
responseHandler.responseReceived(msg) responseHandler.responseReceived(msg)
responseReceived = true
} }
is LastMemcacheContent -> { is LastMemcacheContent -> {
responseFinished = true
responseHandler.contentReceived(msg) responseHandler.contentReceived(msg)
pipeline.remove(this) pipeline.remove(this)
pool.release(channel)
} }
is MemcacheContent -> { is MemcacheContent -> {
responseBodyReceived = true
responseHandler.contentReceived(msg) responseHandler.contentReceived(msg)
} }
} }
@@ -165,35 +141,43 @@ class MemcacheClient(
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
connectionClosedByTheRemoteServer = false connectionClosedByTheRemoteServer = false
ctx.close() ctx.close()
pool.release(channel)
responseHandler.exceptionCaught(cause) responseHandler.exceptionCaught(cause)
} }
} }
channel.pipeline() channel.pipeline().addLast(handler)
.addLast("client-handler", handler)
response.complete(object : MemcacheRequestController { response.complete(object : MemcacheRequestController {
private var channelReleased = false
override fun sendRequest(request: BinaryMemcacheRequest) { override fun sendRequest(request: BinaryMemcacheRequest) {
requestBodySize = request.totalBodyLength() - request.keyLength() - request.extrasLength()
channel.writeAndFlush(request) channel.writeAndFlush(request)
requestSent = true
} }
override fun sendContent(content: MemcacheContent) { override fun sendContent(content: MemcacheContent) {
val size = content.content().readableBytes()
channel.writeAndFlush(content).addListener { channel.writeAndFlush(content).addListener {
requestBodyBytesSent += size
requestBodySent = true
if(content is LastMemcacheContent) { if(content is LastMemcacheContent) {
requestFinished = true if(!channelReleased) {
pool.release(channel)
channelReleased = true
log.trace(channel) {
"Channel released"
}
}
} }
} }
} }
override fun exceptionCaught(ex: Throwable) { override fun exceptionCaught(ex: Throwable) {
log.warn(ex.message, ex)
connectionClosedByTheRemoteServer = false connectionClosedByTheRemoteServer = false
channel.close() channel.close()
if(!channelReleased) {
pool.release(channel)
channelReleased = true
log.trace(channel) {
"Channel released"
}
}
} }
}) })
} else { } else {

View File

@@ -21,6 +21,14 @@
</xs:sequence> </xs:sequence>
<xs:attribute name="max-age" type="xs:duration" default="P1D"/> <xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/> <xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000"/>
<xs:attribute name="key-prefix" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
Prepend this string to all the keys inserted in memcache,
useful in case the caching backend is shared with other applications
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="digest" type="xs:token"/> <xs:attribute name="digest" type="xs:token"/>
<xs:attribute name="compression-mode" type="rbcs-memcache:compressionType"/> <xs:attribute name="compression-mode" type="rbcs-memcache:compressionType"/>
<xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/> <xs:attribute name="compression-level" type="rbcs:compressionLevelType" default="-1"/>

View File

@@ -2,12 +2,12 @@ package net.woggioni.rbcs.server.memcache.client
import io.netty.buffer.ByteBufUtil import io.netty.buffer.ByteBufUtil
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.channels.Channels import java.nio.channels.Channels
import kotlin.random.Random import kotlin.random.Random
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class ByteBufferTest { class ByteBufferTest {

View File

@@ -3,27 +3,27 @@ import net.woggioni.rbcs.server.cache.FileSystemCacheProvider;
import net.woggioni.rbcs.server.cache.InMemoryCacheProvider; import net.woggioni.rbcs.server.cache.InMemoryCacheProvider;
module net.woggioni.rbcs.server { module net.woggioni.rbcs.server {
requires java.sql;
requires java.xml; requires java.xml;
requires java.logging;
requires java.naming; requires java.naming;
requires kotlin.stdlib; requires kotlin.stdlib;
requires io.netty.buffer;
requires io.netty.transport;
requires io.netty.codec.http; requires io.netty.codec.http;
requires io.netty.common;
requires io.netty.handler; requires io.netty.handler;
requires io.netty.codec;
requires org.slf4j;
requires net.woggioni.jwo; requires net.woggioni.jwo;
requires net.woggioni.rbcs.common; requires net.woggioni.rbcs.common;
requires net.woggioni.rbcs.api; requires net.woggioni.rbcs.api;
requires io.netty.codec.compression;
requires io.netty.transport;
requires io.netty.buffer;
requires io.netty.common;
requires io.netty.codec;
requires org.slf4j;
exports net.woggioni.rbcs.server; exports net.woggioni.rbcs.server;
opens net.woggioni.rbcs.server; opens net.woggioni.rbcs.server;
opens net.woggioni.rbcs.server.schema; opens net.woggioni.rbcs.server.schema;
uses CacheProvider; uses CacheProvider;
provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider; provides CacheProvider with FileSystemCacheProvider, InMemoryCacheProvider;
} }

View File

@@ -11,7 +11,8 @@ import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelInitializer import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPromise import io.netty.channel.ChannelPromise
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.MultiThreadIoEventLoopGroup
import io.netty.channel.nio.NioIoHandler
import io.netty.channel.socket.DatagramChannel import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.ServerSocketChannel import io.netty.channel.socket.ServerSocketChannel
import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.SocketChannel
@@ -21,6 +22,7 @@ import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.compression.CompressionOptions import io.netty.handler.codec.compression.CompressionOptions
import io.netty.handler.codec.http.DefaultHttpContent import io.netty.handler.codec.http.DefaultHttpContent
import io.netty.handler.codec.http.HttpContentCompressor import io.netty.handler.codec.http.HttpContentCompressor
import io.netty.handler.codec.http.HttpDecoderConfig
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpServerCodec import io.netty.handler.codec.http.HttpServerCodec
@@ -33,8 +35,25 @@ import io.netty.handler.timeout.IdleState
import io.netty.handler.timeout.IdleStateEvent import io.netty.handler.timeout.IdleStateEvent
import io.netty.handler.timeout.IdleStateHandler import io.netty.handler.timeout.IdleStateHandler
import io.netty.util.AttributeKey import io.netty.util.AttributeKey
import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup import io.netty.util.concurrent.EventExecutorGroup
import java.io.OutputStream
import java.net.InetSocketAddress
import java.nio.file.Files
import java.nio.file.Path
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.Instant
import java.util.Arrays
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.naming.ldap.LdapName
import javax.net.ssl.SSLPeerUnverifiedException
import net.woggioni.rbcs.api.AsyncCloseable import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.exception.ConfigurationException import net.woggioni.rbcs.api.exception.ConfigurationException
@@ -54,28 +73,10 @@ import net.woggioni.rbcs.server.configuration.Parser
import net.woggioni.rbcs.server.configuration.Serializer import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.exception.ExceptionHandler import net.woggioni.rbcs.server.exception.ExceptionHandler
import net.woggioni.rbcs.server.handler.MaxRequestSizeHandler import net.woggioni.rbcs.server.handler.MaxRequestSizeHandler
import net.woggioni.rbcs.server.handler.ReadTriggerDuplexHandler
import net.woggioni.rbcs.server.handler.ServerHandler import net.woggioni.rbcs.server.handler.ServerHandler
import net.woggioni.rbcs.server.handler.TraceHandler
import net.woggioni.rbcs.server.throttling.BucketManager import net.woggioni.rbcs.server.throttling.BucketManager
import net.woggioni.rbcs.server.throttling.ThrottlingHandler import net.woggioni.rbcs.server.throttling.ThrottlingHandler
import java.io.OutputStream
import java.net.InetSocketAddress
import java.nio.file.Files
import java.nio.file.Path
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.Instant
import java.util.Arrays
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.naming.ldap.LdapName
import javax.net.ssl.SSLPeerUnverifiedException
class RemoteBuildCacheServer(private val cfg: Configuration) { class RemoteBuildCacheServer(private val cfg: Configuration) {
@@ -207,7 +208,6 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
private val cfg: Configuration, private val cfg: Configuration,
private val channelFactory : ChannelFactory<SocketChannel>, private val channelFactory : ChannelFactory<SocketChannel>,
private val datagramChannelFactory : ChannelFactory<DatagramChannel>, private val datagramChannelFactory : ChannelFactory<DatagramChannel>,
private val eventExecutorGroup: EventExecutorGroup
) : ChannelInitializer<Channel>(), AsyncCloseable { ) : ChannelInitializer<Channel>(), AsyncCloseable {
companion object { companion object {
@@ -298,6 +298,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
"Closed connection ${ch.id().asShortText()} with ${ch.remoteAddress()}" "Closed connection ${ch.id().asShortText()} with ${ch.remoteAddress()}"
} }
} }
ch.config().isAutoRead = false
val pipeline = ch.pipeline() val pipeline = ch.pipeline()
cfg.connection.also { conn -> cfg.connection.also { conn ->
val readIdleTimeout = conn.readIdleTimeout.toMillis() val readIdleTimeout = conn.readIdleTimeout.toMillis()
@@ -340,24 +341,27 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
sslContext?.newHandler(ch.alloc())?.also { sslContext?.newHandler(ch.alloc())?.also {
pipeline.addLast(SSL_HANDLER_NAME, it) pipeline.addLast(SSL_HANDLER_NAME, it)
} }
pipeline.addLast(HttpServerCodec()) val httpDecoderConfig = HttpDecoderConfig().apply {
maxChunkSize = cfg.connection.chunkSize
}
pipeline.addLast(HttpServerCodec(httpDecoderConfig))
pipeline.addLast(ReadTriggerDuplexHandler.NAME, ReadTriggerDuplexHandler())
pipeline.addLast(MaxRequestSizeHandler.NAME, MaxRequestSizeHandler(cfg.connection.maxRequestSize)) pipeline.addLast(MaxRequestSizeHandler.NAME, MaxRequestSizeHandler(cfg.connection.maxRequestSize))
pipeline.addLast(HttpChunkContentCompressor(1024)) pipeline.addLast(HttpChunkContentCompressor(1024))
pipeline.addLast(ChunkedWriteHandler()) pipeline.addLast(ChunkedWriteHandler())
authenticator?.let { authenticator?.let {
pipeline.addLast(it) pipeline.addLast(it)
} }
pipeline.addLast(ThrottlingHandler(bucketManager, cfg.connection)) pipeline.addLast(ThrottlingHandler(bucketManager,cfg.rateLimiter, cfg.connection))
val serverHandler = let { val serverHandler = let {
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/")) val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
ServerHandler(prefix) ServerHandler(prefix) {
cacheHandlerFactory.newHandler(cfg, ch.eventLoop(), channelFactory, datagramChannelFactory)
}
} }
pipeline.addLast(eventExecutorGroup, ServerHandler.NAME, serverHandler) pipeline.addLast(ServerHandler.NAME, serverHandler)
pipeline.addLast(ExceptionHandler.NAME, ExceptionHandler)
pipeline.addLast(cacheHandlerFactory.newHandler(ch.eventLoop(), channelFactory, datagramChannelFactory))
pipeline.addLast(TraceHandler)
pipeline.addLast(ExceptionHandler)
} }
override fun asyncClose() = cacheHandlerFactory.asyncClose() override fun asyncClose() = cacheHandlerFactory.asyncClose()
@@ -436,20 +440,13 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
fun run(): ServerHandle { fun run(): ServerHandle {
// Create the multithreaded event loops for the server // Create the multithreaded event loops for the server
val bossGroup = NioEventLoopGroup(1) val bossGroup = MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory())
val channelFactory = ChannelFactory<SocketChannel> { NioSocketChannel() } val channelFactory = ChannelFactory<SocketChannel> { NioSocketChannel() }
val datagramChannelFactory = ChannelFactory<DatagramChannel> { NioDatagramChannel() } val datagramChannelFactory = ChannelFactory<DatagramChannel> { NioDatagramChannel() }
val serverChannelFactory = ChannelFactory<ServerSocketChannel> { NioServerSocketChannel() } val serverChannelFactory = ChannelFactory<ServerSocketChannel> { NioServerSocketChannel() }
val workerGroup = NioEventLoopGroup(0) val workerGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory())
val eventExecutorGroup = run {
val threadFactory = if (cfg.eventExecutor.isUseVirtualThreads) { val serverInitializer = ServerInitializer(cfg, channelFactory, datagramChannelFactory)
Thread.ofVirtual().factory()
} else {
null
}
DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors(), threadFactory)
}
val serverInitializer = ServerInitializer(cfg, channelFactory, datagramChannelFactory, workerGroup)
val bootstrap = ServerBootstrap().apply { val bootstrap = ServerBootstrap().apply {
// Configure the server // Configure the server
group(bossGroup, workerGroup) group(bossGroup, workerGroup)
@@ -470,7 +467,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) {
return ServerHandle( return ServerHandle(
httpChannel.closeFuture(), httpChannel.closeFuture(),
bossGroup, bossGroup,
setOf(workerGroup, eventExecutorGroup), setOf(workerGroup),
serverInitializer serverInitializer
) )
} }

View File

@@ -1,9 +1,5 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import net.woggioni.jwo.JWO
import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.createLogger
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.ObjectInputStream import java.io.ObjectInputStream
@@ -20,6 +16,10 @@ import java.nio.file.attribute.BasicFileAttributes
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import net.woggioni.jwo.JWO
import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.createLogger
class FileSystemCache( class FileSystemCache(
val root: Path, val root: Path,

View File

@@ -4,12 +4,12 @@ import io.netty.channel.ChannelFactory
import io.netty.channel.EventLoopGroup import io.netty.channel.EventLoopGroup
import io.netty.channel.socket.DatagramChannel import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.SocketChannel
import java.nio.file.Path
import java.time.Duration
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.rbcs.api.CacheHandlerFactory import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS import net.woggioni.rbcs.common.RBCS
import java.nio.file.Path
import java.time.Duration
data class FileSystemCacheConfiguration( data class FileSystemCacheConfiguration(
val root: Path?, val root: Path?,
@@ -17,7 +17,6 @@ data class FileSystemCacheConfiguration(
val digestAlgorithm : String?, val digestAlgorithm : String?,
val compressionEnabled: Boolean, val compressionEnabled: Boolean,
val compressionLevel: Int, val compressionLevel: Int,
val chunkSize: Int,
) : Configuration.Cache { ) : Configuration.Cache {
override fun materialize() = object : CacheHandlerFactory { override fun materialize() = object : CacheHandlerFactory {
@@ -26,10 +25,11 @@ data class FileSystemCacheConfiguration(
override fun asyncClose() = cache.asyncClose() override fun asyncClose() = cache.asyncClose()
override fun newHandler( override fun newHandler(
cfg : Configuration,
eventLoop: EventLoopGroup, eventLoop: EventLoopGroup,
socketChannelFactory: ChannelFactory<SocketChannel>, socketChannelFactory: ChannelFactory<SocketChannel>,
datagramChannelFactory: ChannelFactory<DatagramChannel> datagramChannelFactory: ChannelFactory<DatagramChannel>
) = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, chunkSize) ) = FileSystemCacheHandler(cache, digestAlgorithm, compressionEnabled, compressionLevel, cfg.connection.chunkSize)
} }
override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI override fun getNamespaceURI() = RBCS.RBCS_NAMESPACE_URI

View File

@@ -2,9 +2,14 @@ package net.woggioni.rbcs.server.cache
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.LastHttpContent import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.stream.ChunkedNioFile import io.netty.handler.stream.ChunkedNioFile
import java.nio.channels.Channels
import java.util.Base64
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream
import net.woggioni.rbcs.api.CacheHandler
import net.woggioni.rbcs.api.message.CacheMessage import net.woggioni.rbcs.api.message.CacheMessage
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest
@@ -14,11 +19,6 @@ import net.woggioni.rbcs.api.message.CacheMessage.CacheValueFoundResponse
import net.woggioni.rbcs.api.message.CacheMessage.CacheValueNotFoundResponse import net.woggioni.rbcs.api.message.CacheMessage.CacheValueNotFoundResponse
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
import net.woggioni.rbcs.common.RBCS.processCacheKey import net.woggioni.rbcs.common.RBCS.processCacheKey
import java.nio.channels.Channels
import java.util.Base64
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream
class FileSystemCacheHandler( class FileSystemCacheHandler(
private val cache: FileSystemCache, private val cache: FileSystemCache,
@@ -26,12 +26,18 @@ class FileSystemCacheHandler(
private val compressionEnabled: Boolean, private val compressionEnabled: Boolean,
private val compressionLevel: Int, private val compressionLevel: Int,
private val chunkSize: Int private val chunkSize: Int
) : SimpleChannelInboundHandler<CacheMessage>() { ) : CacheHandler() {
private interface InProgressRequest{
}
private class InProgressGetRequest(val request : CacheGetRequest) : InProgressRequest
private inner class InProgressPutRequest( private inner class InProgressPutRequest(
val key : String, val key : String,
private val fileSink : FileSystemCache.FileSink private val fileSink : FileSystemCache.FileSink
) { ) : InProgressRequest {
private val stream = Channels.newOutputStream(fileSink.channel).let { private val stream = Channels.newOutputStream(fileSink.channel).let {
if (compressionEnabled) { if (compressionEnabled) {
@@ -55,7 +61,7 @@ class FileSystemCacheHandler(
} }
} }
private var inProgressPutRequest: InProgressPutRequest? = null private var inProgressRequest: InProgressRequest? = null
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) { override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
when (msg) { when (msg) {
@@ -68,55 +74,64 @@ class FileSystemCacheHandler(
} }
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) { private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
val key = String(Base64.getUrlEncoder().encode(processCacheKey(msg.key, digestAlgorithm))) inProgressRequest = InProgressGetRequest(msg)
cache.get(key)?.also { entryValue ->
ctx.writeAndFlush(CacheValueFoundResponse(msg.key, entryValue.metadata))
entryValue.channel.let { channel ->
if(compressionEnabled) {
InflaterInputStream(Channels.newInputStream(channel)).use { stream ->
outerLoop@
while (true) {
val buf = ctx.alloc().heapBuffer(chunkSize)
while(buf.readableBytes() < chunkSize) {
val read = buf.writeBytes(stream, chunkSize)
if(read < 0) {
ctx.writeAndFlush(LastCacheContent(buf))
break@outerLoop
}
}
ctx.writeAndFlush(CacheContent(buf))
}
}
} else {
ctx.writeAndFlush(ChunkedNioFile(channel, entryValue.offset, entryValue.size - entryValue.offset, chunkSize))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
}
}
} ?: ctx.writeAndFlush(CacheValueNotFoundResponse())
} }
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) { private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
val key = String(Base64.getUrlEncoder().encode(processCacheKey(msg.key, digestAlgorithm))) val key = String(Base64.getUrlEncoder().encode(processCacheKey(msg.key, null, digestAlgorithm)))
val sink = cache.put(key, msg.metadata) val sink = cache.put(key, msg.metadata)
inProgressPutRequest = InProgressPutRequest(msg.key, sink) inProgressRequest = InProgressPutRequest(msg.key, sink)
} }
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) { private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
inProgressPutRequest!!.write(msg.content()) val request = inProgressRequest
if(request is InProgressPutRequest) {
request.write(msg.content())
}
} }
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) { private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
inProgressPutRequest?.let { request -> when(val request = inProgressRequest) {
inProgressPutRequest = null is InProgressPutRequest -> {
request.write(msg.content()) inProgressRequest = null
request.commit() request.write(msg.content())
ctx.writeAndFlush(CachePutResponse(request.key)) request.commit()
sendMessageAndFlush(ctx, CachePutResponse(request.key))
}
is InProgressGetRequest -> {
val key = String(Base64.getUrlEncoder().encode(processCacheKey(request.request.key, null, digestAlgorithm)))
cache.get(key)?.also { entryValue ->
sendMessageAndFlush(ctx, CacheValueFoundResponse(request.request.key, entryValue.metadata))
entryValue.channel.let { channel ->
if(compressionEnabled) {
InflaterInputStream(Channels.newInputStream(channel)).use { stream ->
outerLoop@
while (true) {
val buf = ctx.alloc().heapBuffer(chunkSize)
while(buf.readableBytes() < chunkSize) {
val read = buf.writeBytes(stream, chunkSize)
if(read < 0) {
sendMessageAndFlush(ctx, LastCacheContent(buf))
break@outerLoop
}
}
sendMessageAndFlush(ctx, CacheContent(buf))
}
}
} else {
sendMessage(ctx, ChunkedNioFile(channel, entryValue.offset, entryValue.size - entryValue.offset, chunkSize))
sendMessageAndFlush(ctx, LastHttpContent.EMPTY_LAST_CONTENT)
}
}
} ?: sendMessageAndFlush(ctx, CacheValueNotFoundResponse(key))
}
} }
} }
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
inProgressPutRequest?.rollback() (inProgressRequest as? InProgressPutRequest)?.rollback()
super.exceptionCaught(ctx, cause) super.exceptionCaught(ctx, cause)
} }
} }

View File

@@ -1,14 +1,14 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import java.nio.file.Path
import java.time.Duration
import java.util.zip.Deflater
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.rbcs.api.CacheProvider
import net.woggioni.rbcs.common.RBCS import net.woggioni.rbcs.common.RBCS
import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import java.nio.file.Path
import java.time.Duration
import java.util.zip.Deflater
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> { class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
@@ -31,9 +31,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
?.let(String::toInt) ?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION ?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.renderAttribute("digest") val digestAlgorithm = el.renderAttribute("digest")
val chunkSize = el.renderAttribute("chunk-size")
?.let(Integer::decode)
?: 0x10000
return FileSystemCacheConfiguration( return FileSystemCacheConfiguration(
path, path,
@@ -41,7 +38,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
digestAlgorithm, digestAlgorithm,
enableCompression, enableCompression,
compressionLevel, compressionLevel,
chunkSize
) )
} }
@@ -63,7 +59,6 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
}?.let { }?.let {
attr("compression-level", it.toString()) attr("compression-level", it.toString())
} }
attr("chunk-size", chunkSize.toString())
} }
result result
} }

View File

@@ -1,9 +1,5 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import io.netty.buffer.ByteBuf
import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.createLogger
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.PriorityQueue import java.util.PriorityQueue
@@ -11,6 +7,9 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import net.woggioni.rbcs.api.AsyncCloseable
import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.common.createLogger
private class CacheKey(private val value: ByteArray) { private class CacheKey(private val value: ByteArray) {
override fun equals(other: Any?) = if (other is CacheKey) { override fun equals(other: Any?) = if (other is CacheKey) {
@@ -22,7 +21,7 @@ private class CacheKey(private val value: ByteArray) {
class CacheEntry( class CacheEntry(
val metadata: CacheValueMetadata, val metadata: CacheValueMetadata,
val content: ByteBuf val content: ByteArray
) )
class InMemoryCache( class InMemoryCache(
@@ -66,8 +65,6 @@ class InMemoryCache(
val removed = map.remove(el.key, value) val removed = map.remove(el.key, value)
if (removed) { if (removed) {
updateSizeAfterRemoval(value.content) updateSizeAfterRemoval(value.content)
//Decrease the reference count for map
value.content.release()
} }
} else { } else {
removalQueue.offer(el) removalQueue.offer(el)
@@ -75,6 +72,7 @@ class InMemoryCache(
cond.await(interval.toMillis(), TimeUnit.MILLISECONDS) cond.await(interval.toMillis(), TimeUnit.MILLISECONDS)
} }
} }
map.clear()
} }
complete(null) complete(null)
} catch (ex: Throwable) { } catch (ex: Throwable) {
@@ -91,15 +89,13 @@ class InMemoryCache(
val removed = map.remove(el.key, value) val removed = map.remove(el.key, value)
if (removed) { if (removed) {
val newSize = updateSizeAfterRemoval(value.content) val newSize = updateSizeAfterRemoval(value.content)
//Decrease the reference count for map
value.content.release()
return newSize return newSize
} }
} }
} }
private fun updateSizeAfterRemoval(removed: ByteBuf): Long { private fun updateSizeAfterRemoval(removed: ByteArray): Long {
mapSize -= removed.readableBytes() mapSize -= removed.size
return mapSize return mapSize
} }
@@ -113,7 +109,7 @@ class InMemoryCache(
fun get(key: ByteArray) = lock.readLock().withLock { fun get(key: ByteArray) = lock.readLock().withLock {
map[CacheKey(key)]?.run { map[CacheKey(key)]?.run {
CacheEntry(metadata, content.retainedDuplicate()) CacheEntry(metadata, content)
} }
} }
@@ -123,12 +119,8 @@ class InMemoryCache(
) { ) {
val cacheKey = CacheKey(key) val cacheKey = CacheKey(key)
lock.writeLock().withLock { lock.writeLock().withLock {
val oldSize = map.put(cacheKey, value)?.let { old -> val oldSize = map.put(cacheKey, value)?.content?.size ?: 0
val result = old.content.readableBytes() val delta = value.content.size - oldSize
old.content.release()
result
} ?: 0
val delta = value.content.readableBytes() - oldSize
mapSize += delta mapSize += delta
removalQueue.offer(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge))) removalQueue.offer(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
while (mapSize > maxSize) { while (mapSize > maxSize) {
@@ -136,4 +128,4 @@ class InMemoryCache(
} }
} }
} }
} }

View File

@@ -4,11 +4,10 @@ import io.netty.channel.ChannelFactory
import io.netty.channel.EventLoopGroup import io.netty.channel.EventLoopGroup
import io.netty.channel.socket.DatagramChannel import io.netty.channel.socket.DatagramChannel
import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.SocketChannel
import io.netty.util.concurrent.Future import java.time.Duration
import net.woggioni.rbcs.api.CacheHandlerFactory import net.woggioni.rbcs.api.CacheHandlerFactory
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS import net.woggioni.rbcs.common.RBCS
import java.time.Duration
data class InMemoryCacheConfiguration( data class InMemoryCacheConfiguration(
val maxAge: Duration, val maxAge: Duration,
@@ -16,7 +15,6 @@ data class InMemoryCacheConfiguration(
val digestAlgorithm : String?, val digestAlgorithm : String?,
val compressionEnabled: Boolean, val compressionEnabled: Boolean,
val compressionLevel: Int, val compressionLevel: Int,
val chunkSize : Int
) : Configuration.Cache { ) : Configuration.Cache {
override fun materialize() = object : CacheHandlerFactory { override fun materialize() = object : CacheHandlerFactory {
private val cache = InMemoryCache(maxAge, maxSize) private val cache = InMemoryCache(maxAge, maxSize)
@@ -24,6 +22,7 @@ data class InMemoryCacheConfiguration(
override fun asyncClose() = cache.asyncClose() override fun asyncClose() = cache.asyncClose()
override fun newHandler( override fun newHandler(
cfg : Configuration,
eventLoop: EventLoopGroup, eventLoop: EventLoopGroup,
socketChannelFactory: ChannelFactory<SocketChannel>, socketChannelFactory: ChannelFactory<SocketChannel>,
datagramChannelFactory: ChannelFactory<DatagramChannel> datagramChannelFactory: ChannelFactory<DatagramChannel>

View File

@@ -2,7 +2,12 @@ package net.woggioni.rbcs.server.cache
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterOutputStream
import net.woggioni.rbcs.api.CacheHandler
import net.woggioni.rbcs.api.message.CacheMessage import net.woggioni.rbcs.api.message.CacheMessage
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest import net.woggioni.rbcs.api.message.CacheMessage.CacheGetRequest
@@ -13,18 +18,23 @@ import net.woggioni.rbcs.api.message.CacheMessage.CacheValueNotFoundResponse
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
import net.woggioni.rbcs.common.ByteBufOutputStream import net.woggioni.rbcs.common.ByteBufOutputStream
import net.woggioni.rbcs.common.RBCS.processCacheKey import net.woggioni.rbcs.common.RBCS.processCacheKey
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterOutputStream
class InMemoryCacheHandler( class InMemoryCacheHandler(
private val cache: InMemoryCache, private val cache: InMemoryCache,
private val digestAlgorithm: String?, private val digestAlgorithm: String?,
private val compressionEnabled: Boolean, private val compressionEnabled: Boolean,
private val compressionLevel: Int private val compressionLevel: Int
) : SimpleChannelInboundHandler<CacheMessage>() { ) : CacheHandler() {
private interface InProgressPutRequest : AutoCloseable { private interface InProgressRequest : AutoCloseable {
}
private class InProgressGetRequest(val request: CacheGetRequest) : InProgressRequest {
override fun close() {
}
}
private interface InProgressPutRequest : InProgressRequest {
val request: CachePutRequest val request: CachePutRequest
val buf: ByteBuf val buf: ByteBuf
@@ -33,18 +43,14 @@ class InMemoryCacheHandler(
private inner class InProgressPlainPutRequest(ctx: ChannelHandlerContext, override val request: CachePutRequest) : private inner class InProgressPlainPutRequest(ctx: ChannelHandlerContext, override val request: CachePutRequest) :
InProgressPutRequest { InProgressPutRequest {
override val buf = ctx.alloc().compositeBuffer() override val buf = ctx.alloc().compositeHeapBuffer()
private val stream = ByteBufOutputStream(buf).let {
if (compressionEnabled) {
DeflaterOutputStream(it, Deflater(compressionLevel))
} else {
it
}
}
override fun append(buf: ByteBuf) { override fun append(buf: ByteBuf) {
this.buf.addComponent(true, buf.retain()) if (buf.isDirect) {
this.buf.writeBytes(buf)
} else {
this.buf.addComponent(true, buf.retain())
}
} }
override fun close() { override fun close() {
@@ -72,7 +78,7 @@ class InMemoryCacheHandler(
} }
} }
private var inProgressPutRequest: InProgressPutRequest? = null private var inProgressRequest: InProgressRequest? = null
override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) { override fun channelRead0(ctx: ChannelHandlerContext, msg: CacheMessage) {
when (msg) { when (msg) {
@@ -85,24 +91,11 @@ class InMemoryCacheHandler(
} }
private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) { private fun handleGetRequest(ctx: ChannelHandlerContext, msg: CacheGetRequest) {
cache.get(processCacheKey(msg.key, digestAlgorithm))?.let { value -> inProgressRequest = InProgressGetRequest(msg)
ctx.writeAndFlush(CacheValueFoundResponse(msg.key, value.metadata))
if (compressionEnabled) {
val buf = ctx.alloc().heapBuffer()
InflaterOutputStream(ByteBufOutputStream(buf)).use {
value.content.readBytes(it, value.content.readableBytes())
value.content.release()
buf.retain()
}
ctx.writeAndFlush(LastCacheContent(buf))
} else {
ctx.writeAndFlush(LastCacheContent(value.content))
}
} ?: ctx.writeAndFlush(CacheValueNotFoundResponse())
} }
private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) { private fun handlePutRequest(ctx: ChannelHandlerContext, msg: CachePutRequest) {
inProgressPutRequest = if(compressionEnabled) { inProgressRequest = if (compressionEnabled) {
InProgressCompressedPutRequest(ctx, msg) InProgressCompressedPutRequest(ctx, msg)
} else { } else {
InProgressPlainPutRequest(ctx, msg) InProgressPlainPutRequest(ctx, msg)
@@ -110,27 +103,55 @@ class InMemoryCacheHandler(
} }
private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) { private fun handleCacheContent(ctx: ChannelHandlerContext, msg: CacheContent) {
inProgressPutRequest?.append(msg.content()) val req = inProgressRequest
if (req is InProgressPutRequest) {
req.append(msg.content())
}
} }
private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) { private fun handleLastCacheContent(ctx: ChannelHandlerContext, msg: LastCacheContent) {
handleCacheContent(ctx, msg) handleCacheContent(ctx, msg)
inProgressPutRequest?.let { inProgressRequest -> when (val req = inProgressRequest) {
inProgressPutRequest = null is InProgressGetRequest -> {
val buf = inProgressRequest.buf // this.inProgressRequest = null
buf.retain() cache.get(processCacheKey(req.request.key, null, digestAlgorithm))?.let { value ->
inProgressRequest.close() sendMessageAndFlush(ctx, CacheValueFoundResponse(req.request.key, value.metadata))
val cacheKey = processCacheKey(inProgressRequest.request.key, digestAlgorithm) if (compressionEnabled) {
cache.put(cacheKey, CacheEntry(inProgressRequest.request.metadata, buf)) val buf = ctx.alloc().heapBuffer()
ctx.writeAndFlush(CachePutResponse(inProgressRequest.request.key)) InflaterOutputStream(ByteBufOutputStream(buf)).use {
it.write(value.content)
buf.retain()
}
sendMessage(ctx, LastCacheContent(buf))
} else {
val buf = ctx.alloc().heapBuffer()
ByteBufOutputStream(buf).use {
it.write(value.content)
buf.retain()
}
sendMessage(ctx, LastCacheContent(buf))
}
} ?: sendMessage(ctx, CacheValueNotFoundResponse(req.request.key))
}
is InProgressPutRequest -> {
this.inProgressRequest = null
val buf = req.buf
buf.retain()
req.close()
val bytes = ByteArray(buf.readableBytes()).also(buf::readBytes)
buf.release()
val cacheKey = processCacheKey(req.request.key, null, digestAlgorithm)
cache.put(cacheKey, CacheEntry(req.request.metadata, bytes))
sendMessageAndFlush(ctx, CachePutResponse(req.request.key))
}
} }
} }
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
inProgressPutRequest?.let { req -> inProgressRequest?.close()
req.buf.release() inProgressRequest = null
inProgressPutRequest = null
}
super.exceptionCaught(ctx, cause) super.exceptionCaught(ctx, cause)
} }
} }

View File

@@ -1,13 +1,13 @@
package net.woggioni.rbcs.server.cache package net.woggioni.rbcs.server.cache
import java.time.Duration
import java.util.zip.Deflater
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.rbcs.api.CacheProvider
import net.woggioni.rbcs.common.RBCS import net.woggioni.rbcs.common.RBCS
import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import java.time.Duration
import java.util.zip.Deflater
class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> { class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
@@ -31,16 +31,12 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
?.let(String::toInt) ?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION ?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.renderAttribute("digest") val digestAlgorithm = el.renderAttribute("digest")
val chunkSize = el.renderAttribute("chunk-size")
?.let(Integer::decode)
?: 0x10000
return InMemoryCacheConfiguration( return InMemoryCacheConfiguration(
maxAge, maxAge,
maxSize, maxSize,
digestAlgorithm, digestAlgorithm,
enableCompression, enableCompression,
compressionLevel, compressionLevel,
chunkSize
) )
} }
@@ -60,7 +56,6 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
}?.let { }?.let {
attr("compression-level", it.toString()) attr("compression-level", it.toString())
} }
attr("chunk-size", chunkSize.toString())
} }
result result
} }

View File

@@ -1,8 +1,8 @@
package net.woggioni.rbcs.server.configuration package net.woggioni.rbcs.server.configuration
import java.util.ServiceLoader
import net.woggioni.rbcs.api.CacheProvider import net.woggioni.rbcs.api.CacheProvider
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import java.util.ServiceLoader
object CacheSerializers { object CacheSerializers {
val index = (Configuration::class.java.module.layer?.let { layer -> val index = (Configuration::class.java.module.layer?.let { layer ->

View File

@@ -1,5 +1,8 @@
package net.woggioni.rbcs.server.configuration package net.woggioni.rbcs.server.configuration
import java.nio.file.Paths
import java.time.Duration
import java.time.temporal.ChronoUnit
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Configuration.Authentication import net.woggioni.rbcs.api.Configuration.Authentication
import net.woggioni.rbcs.api.Configuration.BasicAuthentication import net.woggioni.rbcs.api.Configuration.BasicAuthentication
@@ -18,20 +21,19 @@ import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.TypeInfo import org.w3c.dom.TypeInfo
import java.nio.file.Paths
import java.time.Duration
import java.time.temporal.ChronoUnit
object Parser { object Parser {
fun parse(document: Document): Configuration { fun parse(document: Document): Configuration {
val root = document.documentElement val root = document.documentElement
val anonymousUser = User("", null, emptySet(), null) val anonymousUser = User("", null, emptySet(), null)
var connection: Configuration.Connection = Configuration.Connection( var connection: Configuration.Connection = Configuration.Connection(
Duration.of(30, ChronoUnit.SECONDS),
Duration.of(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), 0x4000000,
67108864 0x10000
) )
var rateLimiter = Configuration.RateLimiter(false, 0x100000, 100)
var eventExecutor: Configuration.EventExecutor = Configuration.EventExecutor(true) var eventExecutor: Configuration.EventExecutor = Configuration.EventExecutor(true)
var cache: Cache? = null var cache: Cache? = null
var host = "127.0.0.1" var host = "127.0.0.1"
@@ -119,20 +121,36 @@ object Parser {
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS) ?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
val maxRequestSize = child.renderAttribute("max-request-size") val maxRequestSize = child.renderAttribute("max-request-size")
?.let(Integer::decode) ?: 0x4000000 ?.let(Integer::decode) ?: 0x4000000
val chunkSize = child.renderAttribute("chunk-size")
?.let(Integer::decode) ?: 0x10000
connection = Configuration.Connection( connection = Configuration.Connection(
idleTimeout, idleTimeout,
readIdleTimeout, readIdleTimeout,
writeIdleTimeout, writeIdleTimeout,
maxRequestSize maxRequestSize,
chunkSize
) )
} }
"event-executor" -> { "event-executor" -> {
val useVirtualThread = root.renderAttribute("use-virtual-threads") val useVirtualThread = child.renderAttribute("use-virtual-threads")
?.let(String::toBoolean) ?: true ?.let(String::toBoolean) ?: true
eventExecutor = Configuration.EventExecutor(useVirtualThread) eventExecutor = Configuration.EventExecutor(useVirtualThread)
} }
"rate-limiter" -> {
val delayResponse = child.renderAttribute("delay-response")
?.let(String::toBoolean)
?: false
val messageBufferSize = child.renderAttribute("message-buffer-size")
?.let(Integer::decode)
?: 0x100000
val maxQueuedMessages = child.renderAttribute("max-queued-messages")
?.let(Integer::decode)
?: 100
rateLimiter = Configuration.RateLimiter(delayResponse, messageBufferSize, maxQueuedMessages)
}
"tls" -> { "tls" -> {
var keyStore: KeyStore? = null var keyStore: KeyStore? = null
var trustStore: TrustStore? = null var trustStore: TrustStore? = null
@@ -180,6 +198,7 @@ object Parser {
incomingConnectionsBacklogSize, incomingConnectionsBacklogSize,
serverPath, serverPath,
eventExecutor, eventExecutor,
rateLimiter,
connection, connection,
users, users,
groups, groups,

View File

@@ -40,11 +40,17 @@ object Serializer {
attr("read-idle-timeout", connection.readIdleTimeout.toString()) attr("read-idle-timeout", connection.readIdleTimeout.toString())
attr("write-idle-timeout", connection.writeIdleTimeout.toString()) attr("write-idle-timeout", connection.writeIdleTimeout.toString())
attr("max-request-size", connection.maxRequestSize.toString()) attr("max-request-size", connection.maxRequestSize.toString())
attr("chunk-size", connection.chunkSize.toString())
} }
} }
node("event-executor") { node("event-executor") {
attr("use-virtual-threads", conf.eventExecutor.isUseVirtualThreads.toString()) attr("use-virtual-threads", conf.eventExecutor.isUseVirtualThreads.toString())
} }
node("rate-limiter") {
attr("delay-response", conf.rateLimiter.isDelayRequest.toString())
attr("max-queued-messages", conf.rateLimiter.maxQueuedMessages.toString())
attr("message-buffer-size", conf.rateLimiter.messageBufferSize.toString())
}
val cache = conf.cache val cache = conf.cache
val serializer : CacheProvider<Configuration.Cache> = val serializer : CacheProvider<Configuration.Cache> =
(CacheSerializers.index[cache.namespaceURI to cache.typeName] as? CacheProvider<Configuration.Cache>) ?: throw NotImplementedError() (CacheSerializers.index[cache.namespaceURI to cache.typeName] as? CacheProvider<Configuration.Cache>) ?: throw NotImplementedError()

View File

@@ -13,6 +13,10 @@ import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.timeout.ReadTimeoutException import io.netty.handler.timeout.ReadTimeoutException
import io.netty.handler.timeout.WriteTimeoutException import io.netty.handler.timeout.WriteTimeoutException
import java.net.ConnectException
import java.net.SocketException
import javax.net.ssl.SSLException
import javax.net.ssl.SSLPeerUnverifiedException
import net.woggioni.rbcs.api.exception.CacheException import net.woggioni.rbcs.api.exception.CacheException
import net.woggioni.rbcs.api.exception.ContentTooLargeException import net.woggioni.rbcs.api.exception.ContentTooLargeException
import net.woggioni.rbcs.common.contextLogger import net.woggioni.rbcs.common.contextLogger
@@ -20,13 +24,12 @@ import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.log import net.woggioni.rbcs.common.log
import org.slf4j.event.Level import org.slf4j.event.Level
import org.slf4j.spi.LoggingEventBuilder import org.slf4j.spi.LoggingEventBuilder
import java.net.ConnectException
import java.net.SocketException
import javax.net.ssl.SSLException
import javax.net.ssl.SSLPeerUnverifiedException
@Sharable @Sharable
object ExceptionHandler : ChannelDuplexHandler() { object ExceptionHandler : ChannelDuplexHandler() {
val NAME : String = this::class.java.name
private val log = contextLogger() private val log = contextLogger()
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse( private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(

View File

@@ -0,0 +1,13 @@
package net.woggioni.rbcs.server.handler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.HttpContent
class BlackHoleRequestHandler : SimpleChannelInboundHandler<HttpContent>() {
companion object {
val NAME = BlackHoleRequestHandler::class.java.name
}
override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpContent) {
}
}

View File

@@ -1,28 +0,0 @@
package net.woggioni.rbcs.server.handler
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.HttpContent
import io.netty.handler.codec.http.LastHttpContent
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
@Sharable
object CacheContentHandler : SimpleChannelInboundHandler<HttpContent>() {
val NAME = this::class.java.name
override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpContent) {
when(msg) {
is LastHttpContent -> {
ctx.fireChannelRead(LastCacheContent(msg.content().retain()))
ctx.pipeline().remove(this)
}
else -> ctx.fireChannelRead(CacheContent(msg.content().retain()))
}
}
override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) {
super.exceptionCaught(ctx, cause)
}
}

View File

@@ -0,0 +1,65 @@
package net.woggioni.rbcs.server.handler
import io.netty.buffer.ByteBufHolder
import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPromise
import io.netty.handler.codec.http.LastHttpContent
import net.woggioni.rbcs.common.createLogger
class ReadTriggerDuplexHandler : ChannelDuplexHandler() {
companion object {
val NAME = ReadTriggerDuplexHandler::class.java.name
private val log = createLogger<ReadTriggerDuplexHandler>()
}
private var inFlight = 0
private val messageBuffer = ArrayDeque<Any>()
override fun handlerAdded(ctx: ChannelHandlerContext) {
ctx.read()
}
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if(inFlight > 0) {
messageBuffer.addLast(msg)
} else {
super.channelRead(ctx, msg)
if(msg !is LastHttpContent) {
invokeRead(ctx)
} else {
inFlight += 1
}
}
}
private fun invokeRead(ctx : ChannelHandlerContext) {
if(messageBuffer.isEmpty()) {
ctx.read()
} else {
this.channelRead(ctx, messageBuffer.removeFirst())
}
}
override fun write(
ctx: ChannelHandlerContext,
msg: Any,
promise: ChannelPromise
) {
super.write(ctx, msg, promise)
if(msg is LastHttpContent) {
inFlight -= 1
invokeRead(ctx)
}
}
override fun channelInactive(ctx: ChannelHandlerContext) {
while(messageBuffer.isNotEmpty()) {
val msg = messageBuffer.removeFirst()
if(msg is ByteBufHolder) {
msg.release()
}
}
super.channelInactive(ctx)
}
}

View File

@@ -1,12 +1,14 @@
package net.woggioni.rbcs.server.handler package net.woggioni.rbcs.server.handler
import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPromise import io.netty.channel.ChannelPromise
import io.netty.handler.codec.http.DefaultFullHttpResponse import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.DefaultHttpContent import io.netty.handler.codec.http.DefaultHttpContent
import io.netty.handler.codec.http.DefaultHttpResponse import io.netty.handler.codec.http.DefaultHttpResponse
import io.netty.handler.codec.http.DefaultLastHttpContent import io.netty.handler.codec.http.DefaultLastHttpContent
import io.netty.handler.codec.http.HttpContent
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpHeaders import io.netty.handler.codec.http.HttpHeaders
@@ -15,6 +17,8 @@ import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpUtil import io.netty.handler.codec.http.HttpUtil
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.codec.http.LastHttpContent
import java.nio.file.Path
import net.woggioni.rbcs.api.CacheValueMetadata import net.woggioni.rbcs.api.CacheValueMetadata
import net.woggioni.rbcs.api.message.CacheMessage import net.woggioni.rbcs.api.message.CacheMessage
import net.woggioni.rbcs.api.message.CacheMessage.CacheContent import net.woggioni.rbcs.api.message.CacheMessage.CacheContent
@@ -27,15 +31,14 @@ import net.woggioni.rbcs.api.message.CacheMessage.LastCacheContent
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.common.warn import net.woggioni.rbcs.common.warn
import java.nio.file.Path import net.woggioni.rbcs.server.exception.ExceptionHandler
import java.util.Locale
class ServerHandler(private val serverPrefix: Path) : class ServerHandler(private val serverPrefix: Path, private val cacheHandlerSupplier : () -> ChannelHandler) :
ChannelDuplexHandler() { ChannelDuplexHandler() {
companion object { companion object {
private val log = createLogger<ServerHandler>() private val log = createLogger<ServerHandler>()
val NAME = this::class.java.name val NAME = ServerHandler::class.java.name
} }
private var httpVersion = HttpVersion.HTTP_1_1 private var httpVersion = HttpVersion.HTTP_1_1
@@ -59,20 +62,36 @@ class ServerHandler(private val serverPrefix: Path) :
} }
} }
private var cacheRequestInProgress : Boolean = false
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
when (msg) { when (msg) {
is HttpRequest -> handleRequest(ctx, msg) is HttpRequest -> handleRequest(ctx, msg)
is HttpContent -> {
if(cacheRequestInProgress) {
if(msg is LastHttpContent) {
super.channelRead(ctx, LastCacheContent(msg.content().retain()))
cacheRequestInProgress = false
} else {
super.channelRead(ctx, CacheContent(msg.content().retain()))
}
msg.release()
} else {
super.channelRead(ctx, msg)
}
}
else -> super.channelRead(ctx, msg) else -> super.channelRead(ctx, msg)
} }
} }
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise?) { override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise?) {
if (msg is CacheMessage) { if (msg is CacheMessage) {
try { try {
when (msg) { when (msg) {
is CachePutResponse -> { is CachePutResponse -> {
log.debug(ctx) {
"Added value for key '${msg.key}' to build cache"
}
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.CREATED) val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.CREATED)
val keyBytes = msg.key.toByteArray(Charsets.UTF_8) val keyBytes = msg.key.toByteArray(Charsets.UTF_8)
response.headers().apply { response.headers().apply {
@@ -88,6 +107,9 @@ class ServerHandler(private val serverPrefix: Path) :
} }
is CacheValueNotFoundResponse -> { is CacheValueNotFoundResponse -> {
log.debug(ctx) {
"Value not found for key '${msg.key}'"
}
val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.NOT_FOUND) val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.NOT_FOUND)
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0 response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
setKeepAliveHeader(response.headers()) setKeepAliveHeader(response.headers())
@@ -95,6 +117,9 @@ class ServerHandler(private val serverPrefix: Path) :
} }
is CacheValueFoundResponse -> { is CacheValueFoundResponse -> {
log.debug(ctx) {
"Retrieved value for key '${msg.key}'"
}
val response = DefaultHttpResponse(httpVersion, HttpResponseStatus.OK) val response = DefaultHttpResponse(httpVersion, HttpResponseStatus.OK)
response.headers().apply { response.headers().apply {
set(HttpHeaderNames.CONTENT_TYPE, msg.metadata.mimeType ?: HttpHeaderValues.APPLICATION_OCTET_STREAM) set(HttpHeaderNames.CONTENT_TYPE, msg.metadata.mimeType ?: HttpHeaderValues.APPLICATION_OCTET_STREAM)
@@ -127,6 +152,8 @@ class ServerHandler(private val serverPrefix: Path) :
} finally { } finally {
resetRequestMetadata() resetRequestMetadata()
} }
} else if(msg is LastHttpContent) {
ctx.write(msg, promise)
} else super.write(ctx, msg, promise) } else super.write(ctx, msg, promise)
} }
@@ -137,13 +164,16 @@ class ServerHandler(private val serverPrefix: Path) :
if (method === HttpMethod.GET) { if (method === HttpMethod.GET) {
val path = Path.of(msg.uri()).normalize() val path = Path.of(msg.uri()).normalize()
if (path.startsWith(serverPrefix)) { if (path.startsWith(serverPrefix)) {
cacheRequestInProgress = true
val relativePath = serverPrefix.relativize(path) val relativePath = serverPrefix.relativize(path)
val key = relativePath.toString() val key : String = relativePath.toString()
ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler) val cacheHandler = cacheHandlerSupplier()
ctx.pipeline().addBefore(ExceptionHandler.NAME, null, cacheHandler)
key.let(::CacheGetRequest) key.let(::CacheGetRequest)
.let(ctx::fireChannelRead) .let(ctx::fireChannelRead)
?: ctx.channel().write(CacheValueNotFoundResponse()) ?: ctx.channel().write(CacheValueNotFoundResponse(key))
} else { } else {
cacheRequestInProgress = false
log.warn(ctx) { log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'" "Got request for unhandled path '${msg.uri()}'"
} }
@@ -154,20 +184,21 @@ class ServerHandler(private val serverPrefix: Path) :
} else if (method === HttpMethod.PUT) { } else if (method === HttpMethod.PUT) {
val path = Path.of(msg.uri()).normalize() val path = Path.of(msg.uri()).normalize()
if (path.startsWith(serverPrefix)) { if (path.startsWith(serverPrefix)) {
cacheRequestInProgress = true
val relativePath = serverPrefix.relativize(path) val relativePath = serverPrefix.relativize(path)
val key = relativePath.toString() val key = relativePath.toString()
log.debug(ctx) { val cacheHandler = cacheHandlerSupplier()
"Added value for key '$key' to build cache" ctx.pipeline().addAfter(NAME, null, cacheHandler)
}
ctx.pipeline().addAfter(NAME, CacheContentHandler.NAME, CacheContentHandler)
path.fileName?.toString() path.fileName?.toString()
?.let { ?.let {
val mimeType = HttpUtil.getMimeType(msg)?.toString() val mimeType = HttpUtil.getMimeType(msg)?.toString()
CachePutRequest(key, CacheValueMetadata(msg.headers().get(HttpHeaderNames.CONTENT_DISPOSITION), mimeType)) CachePutRequest(key, CacheValueMetadata(msg.headers().get(HttpHeaderNames.CONTENT_DISPOSITION), mimeType))
} }
?.let(ctx::fireChannelRead) ?.let(ctx::fireChannelRead)
?: ctx.channel().write(CacheValueNotFoundResponse()) ?: ctx.channel().write(CacheValueNotFoundResponse(key))
} else { } else {
cacheRequestInProgress = false
log.warn(ctx) { log.warn(ctx) {
"Got request for unhandled path '${msg.uri()}'" "Got request for unhandled path '${msg.uri()}'"
} }
@@ -176,8 +207,11 @@ class ServerHandler(private val serverPrefix: Path) :
ctx.writeAndFlush(response) ctx.writeAndFlush(response)
} }
} else if (method == HttpMethod.TRACE) { } else if (method == HttpMethod.TRACE) {
cacheRequestInProgress = false
ctx.pipeline().addAfter(NAME, null, TraceHandler)
super.channelRead(ctx, msg) super.channelRead(ctx, msg)
} else { } else {
cacheRequestInProgress = false
log.warn(ctx) { log.warn(ctx) {
"Got request with unhandled method '${msg.method().name()}'" "Got request with unhandled method '${msg.method().name()}'"
} }
@@ -187,43 +221,7 @@ class ServerHandler(private val serverPrefix: Path) :
} }
} }
data class ContentDisposition(val type: Type?, val fileName: String?) {
enum class Type {
attachment, `inline`;
companion object {
@JvmStatic
fun parse(maybeString: String?) = maybeString.let { s ->
try {
java.lang.Enum.valueOf(Type::class.java, s)
} catch (ex: IllegalArgumentException) {
null
}
}
}
}
companion object {
@JvmStatic
fun parse(contentDisposition: String) : ContentDisposition {
val parts = contentDisposition.split(";").dropLastWhile { it.isEmpty() }.toTypedArray()
val dispositionType = parts[0].trim { it <= ' ' }.let(Type::parse) // Get the type (e.g., attachment)
var filename: String? = null
for (i in 1..<parts.size) {
val part = parts[i].trim { it <= ' ' }
if (part.lowercase(Locale.getDefault()).startsWith("filename=")) {
filename = part.substring("filename=".length).trim { it <= ' ' }.replace("\"", "")
break
}
}
return ContentDisposition(dispositionType, filename)
}
}
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
super.exceptionCaught(ctx, cause) super.exceptionCaught(ctx, cause)
} }
} }

View File

@@ -42,6 +42,7 @@ object TraceHandler : ChannelInboundHandlerAdapter() {
} }
is LastHttpContent -> { is LastHttpContent -> {
ctx.writeAndFlush(msg) ctx.writeAndFlush(msg)
ctx.pipeline().remove(this)
} }
is HttpContent -> ctx.writeAndFlush(msg) is HttpContent -> ctx.writeAndFlush(msg)
else -> super.channelRead(ctx, msg) else -> super.channelRead(ctx, msg)

View File

@@ -1,11 +1,11 @@
package net.woggioni.rbcs.server.throttling package net.woggioni.rbcs.server.throttling
import net.woggioni.jwo.Bucket
import net.woggioni.rbcs.api.Configuration
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.Arrays import java.util.Arrays
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.function.Function import java.util.function.Function
import net.woggioni.jwo.Bucket
import net.woggioni.rbcs.api.Configuration
class BucketManager private constructor( class BucketManager private constructor(
private val bucketsByUser: Map<Configuration.User, List<Bucket>> = HashMap(), private val bucketsByUser: Map<Configuration.User, List<Bucket>> = HashMap(),

View File

@@ -1,32 +1,50 @@
package net.woggioni.rbcs.server.throttling package net.woggioni.rbcs.server.throttling
import io.netty.buffer.ByteBufHolder
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.handler.codec.http.DefaultFullHttpResponse import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.HttpContent import io.netty.handler.codec.http.FullHttpMessage
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.codec.http.LastHttpContent
import java.net.InetSocketAddress
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.ArrayDeque
import java.util.concurrent.TimeUnit
import net.woggioni.jwo.Bucket import net.woggioni.jwo.Bucket
import net.woggioni.jwo.LongMath import net.woggioni.jwo.LongMath
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.createLogger
import net.woggioni.rbcs.common.debug
import net.woggioni.rbcs.server.RemoteBuildCacheServer import net.woggioni.rbcs.server.RemoteBuildCacheServer
import java.net.InetSocketAddress
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
class ThrottlingHandler(
class ThrottlingHandler(private val bucketManager : BucketManager, private val bucketManager: BucketManager,
private val connectionConfiguration : Configuration.Connection) : ChannelInboundHandlerAdapter() { rateLimiterConfiguration: Configuration.RateLimiter,
connectionConfiguration: Configuration.Connection
) : ChannelInboundHandlerAdapter() {
private companion object { private companion object {
private val log = createLogger<ThrottlingHandler>() private val log = createLogger<ThrottlingHandler>()
fun nextAttemptIsWithinThreshold(nextAttemptNanos : Long, waitThreshold : Duration) : Boolean {
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttemptNanos, 100_000_000L) * 100L, ChronoUnit.MILLIS)
return waitDuration < waitThreshold
}
} }
private var queuedContent : MutableList<HttpContent>? = null private class RefusedRequest
private val maxMessageBufferSize = rateLimiterConfiguration.messageBufferSize
private val maxQueuedMessages = rateLimiterConfiguration.maxQueuedMessages
private val delayRequests = rateLimiterConfiguration.isDelayRequest
private var requestBufferSize : Int = 0
private var valveClosed = false
private var queuedContent = ArrayDeque<Any>()
/** /**
* If the suggested waiting time from the bucket is lower than this * If the suggested waiting time from the bucket is lower than this
@@ -39,38 +57,149 @@ class ThrottlingHandler(private val bucketManager : BucketManager,
connectionConfiguration.writeIdleTimeout connectionConfiguration.writeIdleTimeout
).dividedBy(2) ).dividedBy(2)
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if(msg is HttpRequest) { if(valveClosed) {
val buckets = mutableListOf<Bucket>() if(msg !is HttpRequest && msg is ByteBufHolder) {
val user = ctx.channel().attr(RemoteBuildCacheServer.userAttribute).get() val newBufferSize = requestBufferSize + msg.content().readableBytes()
if (user != null) { if(newBufferSize > maxMessageBufferSize || queuedContent.size + 1 > maxQueuedMessages) {
bucketManager.getBucketByUser(user)?.let(buckets::addAll) log.debug {
if (newBufferSize > maxMessageBufferSize) {
"New message part exceeds maxMessageBufferSize, removing previous chunks"
} else {
"New message part exceeds maxQueuedMessages, removing previous chunks"
}
}
// If this message overflows the maxMessageBufferSize,
// then remove the previously enqueued chunks of the request from the deque,
// then discard the message
while(true) {
val tail = queuedContent.last()
if(tail is ByteBufHolder) {
requestBufferSize -= tail.content().readableBytes()
tail.release()
}
queuedContent.removeLast()
if(tail is HttpRequest) {
break
}
}
msg.release()
//Add a placeholder to remember to return a 429 response corresponding to this request
queuedContent.addLast(RefusedRequest())
} else {
//If the message does not overflow maxMessageBufferSize, just add it to the deque
queuedContent.addLast(msg)
requestBufferSize = newBufferSize
}
} else if(msg is HttpRequest && msg is FullHttpMessage){
val newBufferSize = requestBufferSize + msg.content().readableBytes()
// If this message overflows the maxMessageBufferSize,
// discard the message
if(newBufferSize > maxMessageBufferSize || queuedContent.size + 1 > maxQueuedMessages) {
log.debug {
if (newBufferSize > maxMessageBufferSize) {
"New message exceeds maxMessageBufferSize, discarding it"
} else {
"New message exceeds maxQueuedMessages, discarding it"
}
}
msg.release()
//Add a placeholder to remember to return a 429 response corresponding to this request
queuedContent.addLast(RefusedRequest())
} else {
//If the message does not exceed maxMessageBufferSize or maxQueuedMessages, just add it to the deque
queuedContent.addLast(msg)
requestBufferSize = newBufferSize
}
} else {
queuedContent.addLast(msg)
} }
val groups = ctx.channel().attr(RemoteBuildCacheServer.groupAttribute).get() ?: emptySet() } else {
if (groups.isNotEmpty()) { entryPoint(ctx, msg)
groups.forEach { group -> }
bucketManager.getBucketByGroup(group)?.let(buckets::add) }
private fun entryPoint(ctx : ChannelHandlerContext, msg : Any) {
if(msg is RefusedRequest) {
sendThrottledResponse(ctx, null)
if(queuedContent.isEmpty()) {
valveClosed = false
} else {
val head = queuedContent.poll()
if(head is ByteBufHolder) {
requestBufferSize -= head.content().readableBytes()
}
entryPoint(ctx, head)
}
} else if(msg is HttpRequest) {
val nextAttempt = getNextAttempt(ctx)
if (nextAttempt < 0) {
super.channelRead(ctx, msg)
if(msg !is LastHttpContent) {
while (true) {
val head = queuedContent.poll() ?: break
if(head is ByteBufHolder) {
requestBufferSize -= head.content().readableBytes()
}
super.channelRead(ctx, head)
if (head is LastHttpContent) break
}
}
if(queuedContent.isEmpty()) {
valveClosed = false
} else {
val head = queuedContent.poll()
if(head is ByteBufHolder) {
requestBufferSize -= head.content().readableBytes()
}
entryPoint(ctx, head)
}
} else {
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttempt, 100_000_000L) * 100L, ChronoUnit.MILLIS)
if (delayRequests && nextAttemptIsWithinThreshold(nextAttempt, waitThreshold)) {
valveClosed = true
ctx.executor().schedule({
entryPoint(ctx, msg)
}, waitDuration.toMillis(), TimeUnit.MILLISECONDS)
} else {
sendThrottledResponse(ctx, waitDuration)
if(queuedContent.isEmpty()) {
valveClosed = false
} else {
val head = queuedContent.poll()
if(head is ByteBufHolder) {
requestBufferSize -= head.content().readableBytes()
}
entryPoint(ctx, head)
}
} }
} }
if (user == null && groups.isEmpty()) {
bucketManager.getBucketByAddress(ctx.channel().remoteAddress() as InetSocketAddress)?.let(buckets::add)
}
if (buckets.isEmpty()) {
super.channelRead(ctx, msg)
} else {
handleBuckets(buckets, ctx, msg, true)
}
ctx.channel().id()
} else if(msg is HttpContent) {
queuedContent?.add(msg) ?: super.channelRead(ctx, msg)
} else { } else {
super.channelRead(ctx, msg) super.channelRead(ctx, msg)
} }
} }
private fun handleBuckets(buckets: List<Bucket>, ctx: ChannelHandlerContext, msg: Any, delayResponse: Boolean) { /**
* Returns the number amount of milliseconds to wait before the requests can be processed
* or -1 if the request can be performed immediately
*/
private fun getNextAttempt(ctx : ChannelHandlerContext) : Long {
val buckets = mutableListOf<Bucket>()
val user = ctx.channel().attr(RemoteBuildCacheServer.userAttribute).get()
if (user != null) {
bucketManager.getBucketByUser(user)?.let(buckets::addAll)
}
val groups = ctx.channel().attr(RemoteBuildCacheServer.groupAttribute).get() ?: emptySet()
if (groups.isNotEmpty()) {
groups.forEach { group ->
bucketManager.getBucketByGroup(group)?.let(buckets::add)
}
}
if (user == null && groups.isEmpty()) {
bucketManager.getBucketByAddress(ctx.channel().remoteAddress() as InetSocketAddress)?.let(buckets::add)
}
var nextAttempt = -1L var nextAttempt = -1L
for (bucket in buckets) { for (bucket in buckets) {
val bucketNextAttempt = bucket.removeTokensWithEstimate(1) val bucketNextAttempt = bucket.removeTokensWithEstimate(1)
@@ -78,38 +207,19 @@ class ThrottlingHandler(private val bucketManager : BucketManager,
nextAttempt = bucketNextAttempt nextAttempt = bucketNextAttempt
} }
} }
if (nextAttempt < 0) { return nextAttempt
super.channelRead(ctx, msg)
queuedContent?.let {
for(content in it) {
super.channelRead(ctx, content)
}
queuedContent = null
}
} else {
val waitDuration = Duration.of(LongMath.ceilDiv(nextAttempt, 100_000_000L) * 100L, ChronoUnit.MILLIS)
if (delayResponse && waitDuration < waitThreshold) {
this.queuedContent = mutableListOf()
ctx.executor().schedule({
handleBuckets(buckets, ctx, msg, false)
}, waitDuration.toMillis(), TimeUnit.MILLISECONDS)
} else {
this.queuedContent = null
sendThrottledResponse(ctx, waitDuration)
}
}
} }
private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration) { private fun sendThrottledResponse(ctx: ChannelHandlerContext, retryAfter: Duration?) {
val response = DefaultFullHttpResponse( val response = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpVersion.HTTP_1_1,
HttpResponseStatus.TOO_MANY_REQUESTS HttpResponseStatus.TOO_MANY_REQUESTS
) )
response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0 response.headers()[HttpHeaderNames.CONTENT_LENGTH] = 0
retryAfter.seconds.takeIf { retryAfter?.seconds?.takeIf {
it > 0 it > 0
}?.let { }?.let {
response.headers()[HttpHeaderNames.RETRY_AFTER] = retryAfter.seconds response.headers()[HttpHeaderNames.RETRY_AFTER] = it
} }
ctx.writeAndFlush(response) ctx.writeAndFlush(response)

View File

@@ -16,6 +16,7 @@
<xs:element name="bind" type="rbcs:bindType" maxOccurs="1"/> <xs:element name="bind" type="rbcs:bindType" maxOccurs="1"/>
<xs:element name="connection" type="rbcs:connectionType" minOccurs="0" maxOccurs="1"/> <xs:element name="connection" type="rbcs:connectionType" minOccurs="0" maxOccurs="1"/>
<xs:element name="event-executor" type="rbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/> <xs:element name="event-executor" type="rbcs:eventExecutorType" minOccurs="0" maxOccurs="1"/>
<xs:element name="rate-limiter" type="rbcs:rateLimiterType" minOccurs="0" maxOccurs="1"/>
<xs:element name="cache" type="rbcs:cacheType" maxOccurs="1"> <xs:element name="cache" type="rbcs:cacheType" maxOccurs="1">
<xs:annotation> <xs:annotation>
<xs:documentation> <xs:documentation>
@@ -115,6 +116,14 @@
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
</xs:attribute> </xs:attribute>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
<xs:annotation>
<xs:documentation>
Maximum byte size of socket write calls
(reduce it to reduce memory consumption, increase it for increased throughput)
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="eventExecutorType"> <xs:complexType name="eventExecutorType">
@@ -128,6 +137,37 @@
</xs:attribute> </xs:attribute>
</xs:complexType> </xs:complexType>
<xs:complexType name="rateLimiterType">
<xs:attribute name="delay-response" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>
If set to true, the server will delay responses to meet user quotas, otherwise it will simply
return an immediate 429 status code to all requests that exceed the configured quota
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-queued-messages" type="xs:nonNegativeInteger" use="optional" default="100">
<xs:annotation>
<xs:documentation>
Only meaningful when "delay-response" is set to "true",
when a request is delayed, it and all the following messages are queued
as long as "max-queued-messages" is not crossed, all requests that would exceed the
max-queued-message limit are instead discarded and responded with a 429 status code
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="message-buffer-size" type="rbcs:byteSizeType" use="optional" default="0x100000">
<xs:annotation>
<xs:documentation>
Only meaningful when "delay-response" is set to "true",
when a request is delayed, it and all the following requests are buffered
as long as "message-buffer-size" is not crossed, all requests that would exceed the buffer
size are instead discarded and responded with a 429 status code
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="cacheType" abstract="true"/> <xs:complexType name="cacheType" abstract="true"/>
<xs:complexType name="inMemoryCacheType"> <xs:complexType name="inMemoryCacheType">
@@ -175,13 +215,6 @@
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
</xs:attribute> </xs:attribute>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
<xs:annotation>
<xs:documentation>
Maximum byte size of socket write calls
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension> </xs:extension>
</xs:complexContent> </xs:complexContent>
</xs:complexType> </xs:complexType>
@@ -231,14 +264,6 @@
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
</xs:attribute> </xs:attribute>
<xs:attribute name="chunk-size" type="rbcs:byteSizeType" default="0x10000">
<xs:annotation>
<xs:documentation>
Maximum byte size of a cache value that will be stored in memory
(reduce it to reduce memory consumption, increase it for increased throughput)
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension> </xs:extension>
</xs:complexContent> </xs:complexContent>
</xs:complexType> </xs:complexType>

View File

@@ -1,11 +1,5 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer
import java.net.URI import java.net.URI
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@@ -15,6 +9,12 @@ import java.time.temporal.ChronoUnit
import java.util.Base64 import java.util.Base64
import java.util.zip.Deflater import java.util.zip.Deflater
import kotlin.random.Random import kotlin.random.Random
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer
abstract class AbstractBasicAuthServerTest : AbstractServerTest() { abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
@@ -37,11 +37,13 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
50, 50,
serverPath, serverPath,
Configuration.EventExecutor(false), Configuration.EventExecutor(false),
Configuration.RateLimiter(true, 0x100000, 50),
Configuration.Connection( Configuration.Connection(
Duration.of(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
0x1000 0x1000,
0x10000
), ),
users.asSequence().map { it.name to it}.toMap(), users.asSequence().map { it.name to it}.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(), sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
@@ -50,8 +52,7 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() {
maxAge = Duration.ofSeconds(3600 * 24), maxAge = Duration.ofSeconds(3600 * 24),
digestAlgorithm = "MD5", digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION, compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false, compressionEnabled = false
chunkSize = 0x1000
), ),
Configuration.BasicAuthentication(), Configuration.BasicAuthentication(),
null, null,

View File

@@ -1,5 +1,6 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import java.nio.file.Path
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.server.RemoteBuildCacheServer import net.woggioni.rbcs.server.RemoteBuildCacheServer
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
@@ -8,7 +9,6 @@ import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.TestMethodOrder
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)

View File

@@ -1,14 +1,5 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.test.utils.CertificateUtils
import net.woggioni.rbcs.server.test.utils.CertificateUtils.X509Credentials
import org.bouncycastle.asn1.x500.X500Name
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
@@ -25,6 +16,15 @@ import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory import javax.net.ssl.TrustManagerFactory
import kotlin.random.Random import kotlin.random.Random
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer
import net.woggioni.rbcs.server.test.utils.CertificateUtils
import net.woggioni.rbcs.server.test.utils.CertificateUtils.X509Credentials
import org.bouncycastle.asn1.x500.X500Name
abstract class AbstractTlsServerTest : AbstractServerTest() { abstract class AbstractTlsServerTest : AbstractServerTest() {
@@ -143,11 +143,13 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
100, 100,
serverPath, serverPath,
Configuration.EventExecutor(false), Configuration.EventExecutor(false),
Configuration.RateLimiter(true, 0x100000, 50),
Configuration.Connection( Configuration.Connection(
Duration.of(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
0x1000 0x1000,
0x10000
), ),
users.asSequence().map { it.name to it }.toMap(), users.asSequence().map { it.name to it }.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(), sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
@@ -156,7 +158,6 @@ abstract class AbstractTlsServerTest : AbstractServerTest() {
compressionEnabled = false, compressionEnabled = false,
compressionLevel = Deflater.DEFAULT_COMPRESSION, compressionLevel = Deflater.DEFAULT_COMPRESSION,
digestAlgorithm = "MD5", digestAlgorithm = "MD5",
chunkSize = 0x1000
), ),
// InMemoryCacheConfiguration( // InMemoryCacheConfiguration(
// maxAge = Duration.ofSeconds(3600 * 24), // maxAge = Duration.ofSeconds(3600 * 24),

View File

@@ -1,17 +1,17 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import java.time.temporal.ChronoUnit
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.rbcs.api.Role
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import java.time.temporal.ChronoUnit
class BasicAuthServerTest : AbstractBasicAuthServerTest() { class BasicAuthServerTest : AbstractBasicAuthServerTest() {
@@ -154,7 +154,7 @@ class BasicAuthServerTest : AbstractBasicAuthServerTest() {
} }
@Test @Test
@Order(6) @Order(8)
fun getAsAThrottledUser() { fun getAsAThrottledUser() {
val client: HttpClient = HttpClient.newHttpClient() val client: HttpClient = HttpClient.newHttpClient()
@@ -172,7 +172,7 @@ class BasicAuthServerTest : AbstractBasicAuthServerTest() {
} }
@Test @Test
@Order(7) @Order(9)
fun getAsAThrottledUser2() { fun getAsAThrottledUser2() {
val client: HttpClient = HttpClient.newHttpClient() val client: HttpClient = HttpClient.newHttpClient()

View File

@@ -1,5 +1,7 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import java.nio.file.Files
import java.nio.file.Path
import net.woggioni.rbcs.common.RBCS.toUrl import net.woggioni.rbcs.common.RBCS.toUrl
import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory import net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.Xml
@@ -10,8 +12,6 @@ import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource import org.junit.jupiter.params.provider.ValueSource
import org.xml.sax.SAXParseException import org.xml.sax.SAXParseException
import java.nio.file.Files
import java.nio.file.Path
class ConfigurationTest { class ConfigurationTest {

View File

@@ -1,14 +1,14 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.PasswordSecurity.hashPassword import net.woggioni.rbcs.common.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class NoAnonymousUserBasicAuthServerTest : AbstractBasicAuthServerTest() { class NoAnonymousUserBasicAuthServerTest : AbstractBasicAuthServerTest() {

View File

@@ -1,13 +1,13 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class NoAnonymousUserTlsServerTest : AbstractTlsServerTest() { class NoAnonymousUserTlsServerTest : AbstractTlsServerTest() {

View File

@@ -1,14 +1,6 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
@@ -19,6 +11,14 @@ import java.time.temporal.ChronoUnit
import java.util.Base64 import java.util.Base64
import java.util.zip.Deflater import java.util.zip.Deflater
import kotlin.random.Random import kotlin.random.Random
import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.common.RBCS.getFreePort
import net.woggioni.rbcs.common.Xml
import net.woggioni.rbcs.server.cache.InMemoryCacheConfiguration
import net.woggioni.rbcs.server.configuration.Serializer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
class NoAuthServerTest : AbstractServerTest() { class NoAuthServerTest : AbstractServerTest() {
@@ -37,11 +37,13 @@ class NoAuthServerTest : AbstractServerTest() {
100, 100,
serverPath, serverPath,
Configuration.EventExecutor(false), Configuration.EventExecutor(false),
Configuration.RateLimiter(true, 0x100000, 50),
Configuration.Connection( Configuration.Connection(
Duration.of(60, ChronoUnit.SECONDS), Duration.of(60, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
Duration.of(30, ChronoUnit.SECONDS), Duration.of(30, ChronoUnit.SECONDS),
0x1000 0x1000,
0x10000
), ),
emptyMap(), emptyMap(),
emptyMap(), emptyMap(),
@@ -51,7 +53,6 @@ class NoAuthServerTest : AbstractServerTest() {
digestAlgorithm = "MD5", digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION, compressionLevel = Deflater.DEFAULT_COMPRESSION,
maxSize = 0x1000000, maxSize = 0x1000000,
chunkSize = 0x1000
), ),
null, null,
null, null,

View File

@@ -1,15 +1,15 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import net.woggioni.rbcs.api.Configuration import net.woggioni.rbcs.api.Configuration
import net.woggioni.rbcs.api.Role import net.woggioni.rbcs.api.Role
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class TlsServerTest : AbstractTlsServerTest() { class TlsServerTest : AbstractTlsServerTest() {
@@ -166,4 +166,17 @@ class TlsServerTest : AbstractTlsServerTest() {
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode()) Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
println(String(response.body())) println(String(response.body()))
} }
@Test
@Order(10)
fun putAsUnknownUserUser() {
val (key, value) = keyValuePair
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=Unknown user")))
val requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), response.statusCode())
}
} }

View File

@@ -1,8 +1,8 @@
package net.woggioni.rbcs.server.test package net.woggioni.rbcs.server.test
import javax.naming.ldap.LdapName
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import javax.naming.ldap.LdapName
class X500NameTest { class X500NameTest {

View File

@@ -7,9 +7,11 @@
read-idle-timeout="PT10M" read-idle-timeout="PT10M"
write-idle-timeout="PT11M" write-idle-timeout="PT11M"
idle-timeout="PT30M" idle-timeout="PT30M"
max-request-size="101325"/> max-request-size="101325"
chunk-size="0xa910"/>
<event-executor use-virtual-threads="false"/> <event-executor use-virtual-threads="false"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D" chunk-size="0xa910"/> <rate-limiter delay-response="false" message-buffer-size="0x1234" max-queued-messages="13"/>
<cache xs:type="rbcs:fileSystemCacheType" path="/tmp/rbcs" max-age="P7D"/>
<authentication> <authentication>
<none/> <none/>
</authentication> </authentication>

View File

@@ -9,9 +9,11 @@
max-request-size="67108864" max-request-size="67108864"
idle-timeout="PT30S" idle-timeout="PT30S"
read-idle-timeout="PT60S" read-idle-timeout="PT60S"
write-idle-timeout="PT60S"/> write-idle-timeout="PT60S"
chunk-size="123"/>
<event-executor use-virtual-threads="true"/> <event-executor use-virtual-threads="true"/>
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" chunk-size="123"> <rate-limiter delay-response="false" message-buffer-size="12000" max-queued-messages="53"/>
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" key-prefix="some-prefix-string">
<server host="memcached" port="11211"/> <server host="memcached" port="11211"/>
</cache> </cache>
<authorization> <authorization>

View File

@@ -8,9 +8,11 @@
read-idle-timeout="PT10M" read-idle-timeout="PT10M"
write-idle-timeout="PT11M" write-idle-timeout="PT11M"
idle-timeout="PT30M" idle-timeout="PT30M"
max-request-size="101325"/> max-request-size="101325"
chunk-size="456"/>
<event-executor use-virtual-threads="false"/> <event-executor use-virtual-threads="false"/>
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" digest="SHA-256" chunk-size="456" compression-mode="deflate" compression-level="7"> <rate-limiter delay-response="true" message-buffer-size="65432" max-queued-messages="21"/>
<cache xs:type="rbcs-memcache:memcacheCacheType" max-age="P7D" key-prefix="some-prefix-string" digest="SHA-256" compression-mode="deflate" compression-level="7">
<server host="127.0.0.1" port="11211" max-connections="10" connection-timeout="PT20S"/> <server host="127.0.0.1" port="11211" max-connections="10" connection-timeout="PT20S"/>
</cache> </cache>
<authentication> <authentication>

View File

@@ -7,9 +7,10 @@
read-idle-timeout="PT10M" read-idle-timeout="PT10M"
write-idle-timeout="PT11M" write-idle-timeout="PT11M"
idle-timeout="PT30M" idle-timeout="PT30M"
max-request-size="4096"/> max-request-size="4096"
chunk-size="0xa91f"/>
<event-executor use-virtual-threads="false"/> <event-executor use-virtual-threads="false"/>
<cache xs:type="rbcs:inMemoryCacheType" max-age="P7D" chunk-size="0xa91f"/> <cache xs:type="rbcs:inMemoryCacheType" max-age="P7D"/>
<authorization> <authorization>
<users> <users>
<user name="user1" password="password1"> <user name="user1" password="password1">

View File

@@ -53,7 +53,9 @@
<!-- <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool"--> <!-- <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool"-->
<!-- connectionTimeout="20000"--> <!-- connectionTimeout="20000"-->
<!-- redirectPort="8443" />--> <!-- redirectPort="8443" />-->
<Connector port="8080" protocol="HTTP/1.1" <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxKeepAliveRequests="1000000"
keepAliveTimeout="-1"
connectionTimeout="20000" connectionTimeout="20000"
redirectPort="8443" /> redirectPort="8443" />
<!-- A "Connector" using the shared thread pool--> <!-- A "Connector" using the shared thread pool-->

View File

@@ -7,8 +7,6 @@ import jakarta.servlet.annotation.WebServlet
import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServlet
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import net.woggioni.jwo.HttpClient.HttpStatus
import net.woggioni.jwo.JWO
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.file.Path import java.nio.file.Path
@@ -19,6 +17,8 @@ import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Logger import java.util.logging.Logger
import net.woggioni.jwo.HttpClient.HttpStatus
import net.woggioni.jwo.JWO
private class CacheKey(private val value: ByteArray) { private class CacheKey(private val value: ByteArray) {

View File

@@ -1,5 +1,16 @@
pluginManagement { pluginManagement {
repositories { repositories {
// mavenLocal {
// content {
// includeGroup 'net.woggioni.gradle'
// includeGroup 'net.woggioni.gradle.jpms-check'
// includeGroup 'net.woggioni.gradle.lombok'
// includeGroup 'net.woggioni.gradle.jdeps'
// includeGroup 'net.woggioni.gradle.sambal'
// includeGroup 'net.woggioni.gradle.graalvm.jlink'
// includeGroup 'net.woggioni.gradle.graalvm.native-image'
// }
// }
maven { maven {
url = getProperty('gitea.maven.url') url = getProperty('gitea.maven.url')
} }
@@ -19,6 +30,8 @@ dependencyResolutionManagement {
versionCatalogs { versionCatalogs {
catalog { catalog {
from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version') from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version')
// version('my-gradle-plugins', '2025.04.16')
// version('junit', '5.12.0')
} }
} }
} }
@@ -33,3 +46,4 @@ include 'rbcs-client'
include 'rbcs-server' include 'rbcs-server'
include 'rbcs-servlet' include 'rbcs-servlet'
include 'docker' include 'docker'
//include 'bug'