Compare commits

...

5 Commits

Author SHA1 Message Date
5fef1b932e updated lys-catalog version
All checks were successful
CI / build (push) Successful in 2m32s
2025-02-05 21:49:08 +08:00
5e173dbf62 fixed unit tests 2025-02-05 21:24:10 +08:00
53b24e3d54 improved benchmark accuracy 2025-02-05 19:10:25 +08:00
7d0f24fa58 fixed memory leak in InMemoryCache 2025-02-05 19:09:51 +08:00
1b6cf1bd96 fixed memory leak in memcached plugin 2025-02-05 14:41:11 +08:00
16 changed files with 234 additions and 84 deletions

View File

@@ -25,8 +25,12 @@ class GradleBuildCacheServerCli : GbcsCommand() {
companion object { companion object {
@JvmStatic @JvmStatic
fun main(vararg args: String) { fun main(vararg args: String) {
Thread.currentThread().contextClassLoader = GradleBuildCacheServerCli::class.java.classLoader val currentClassLoader = GradleBuildCacheServerCli::class.java.classLoader
GbcsUrlStreamHandlerFactory.install() Thread.currentThread().contextClassLoader = currentClassLoader
if(currentClassLoader.javaClass.name == "net.woggioni.envelope.loader.ModuleClassLoader") {
//We're running in an envelope jar and custom URL protocols won't work
GbcsUrlStreamHandlerFactory.install()
}
val log = contextLogger() val log = contextLogger()
val app = Application.builder("gbcs") val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR") .configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")

View File

@@ -81,6 +81,8 @@ class BenchmarkCommand : GbcsCommand() {
semaphore.release() semaphore.release()
completionCounter.incrementAndGet() completionCounter.incrementAndGet()
} }
} else {
Thread.sleep(0)
} }
} }
@@ -103,23 +105,27 @@ class BenchmarkCommand : GbcsCommand() {
val completionCounter = AtomicLong(0) val completionCounter = AtomicLong(0)
val semaphore = Semaphore(profile.maxConnections * 3) val semaphore = Semaphore(profile.maxConnections * 3)
val start = Instant.now() val start = Instant.now()
entries.forEach { entry -> val it = entries.iterator()
semaphore.acquire() while (completionCounter.get() < entries.size) {
if (it.hasNext()) {
val future = client.get(entry.first).thenApply { val entry = it.next()
if (it == null) { val future = client.get(entry.first).thenApply {
log.error { if (it == null) {
"Missing entry for key '${entry.first}'" log.error {
} "Missing entry for key '${entry.first}'"
} else if (!entry.second.contentEquals(it)) { }
log.error { } else if (!entry.second.contentEquals(it)) {
"Retrieved a value different from what was inserted for key '${entry.first}'" log.error {
"Retrieved a value different from what was inserted for key '${entry.first}'"
}
} }
} }
} future.whenComplete { _, _ ->
future.whenComplete { _, _ -> completionCounter.incrementAndGet()
completionCounter.incrementAndGet() semaphore.release()
semaphore.release() }
} else {
Thread.sleep(0)
} }
} }
val end = Instant.now() val end = Instant.now()

View File

@@ -2,6 +2,7 @@ package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.cli.impl.GbcsCommand import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.converters.DurationConverter
import net.woggioni.gbcs.common.contextLogger import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.debug import net.woggioni.gbcs.common.debug
import net.woggioni.gbcs.common.info import net.woggioni.gbcs.common.info
@@ -13,6 +14,7 @@ import picocli.CommandLine
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration
@CommandLine.Command( @CommandLine.Command(
name = "server", name = "server",
@@ -35,6 +37,14 @@ class ServerCommand(app : Application) : GbcsCommand() {
} }
} }
@CommandLine.Option(
names = ["-t", "--timeout"],
description = ["Exit after the specified time"],
paramLabel = "TIMEOUT",
converter = [DurationConverter::class]
)
private var timeout: Duration? = null
@CommandLine.Option( @CommandLine.Option(
names = ["-c", "--config-file"], names = ["-c", "--config-file"],
description = ["Read the application configuration from this file"], description = ["Read the application configuration from this file"],
@@ -42,10 +52,6 @@ class ServerCommand(app : Application) : GbcsCommand() {
) )
private var configurationFile: Path = findConfigurationFile(app, "gbcs-server.xml") private var configurationFile: Path = findConfigurationFile(app, "gbcs-server.xml")
val configuration : Configuration by lazy {
GradleBuildCacheServer.loadConfiguration(configurationFile)
}
override fun run() { override fun run() {
if (!Files.exists(configurationFile)) { if (!Files.exists(configurationFile)) {
Files.createDirectories(configurationFile.parent) Files.createDirectories(configurationFile.parent)
@@ -61,7 +67,11 @@ class ServerCommand(app : Application) : GbcsCommand() {
} }
} }
val server = GradleBuildCacheServer(configuration) val server = GradleBuildCacheServer(configuration)
server.run().use { server.run().use { server ->
timeout?.let {
Thread.sleep(it)
server.shutdown()
}
} }
} }
} }

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
package net.woggioni.gbcs.common
import io.netty.buffer.ByteBuf
import java.io.InputStream
import java.io.OutputStream
class ByteBufOutputStream(private val buf : ByteBuf) : OutputStream() {
override fun write(b: Int) {
buf.writeByte(b)
}
override fun write(b: ByteArray, off: Int, len: Int) {
buf.writeBytes(b, off, len)
}
override fun close() {
buf.release()
}
}

View File

@@ -0,0 +1,7 @@
package net.woggioni.gbcs.common
class ResourceNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
}
class ModuleNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
}

View File

@@ -36,13 +36,17 @@ class GbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
private class JpmsHandler : URLStreamHandler() { private class JpmsHandler : URLStreamHandler() {
override fun openConnection(u: URL): URLConnection { override fun openConnection(u: URL): URLConnection {
val moduleName = u.host
val thisModule = javaClass.module val thisModule = javaClass.module
val sourceModule = Optional.ofNullable(thisModule) val sourceModule =
.map { obj: Module -> obj.layer } thisModule
.flatMap { layer: ModuleLayer -> ?.let(Module::getLayer)
val moduleName = u.host ?.let { layer: ModuleLayer ->
layer.findModule(moduleName) layer.findModule(moduleName).orElse(null)
}.orElse(thisModule) } ?: if(thisModule.layer == null) {
thisModule
} else throw ModuleNotFoundException("Module '$moduleName' not found")
return JpmsResourceURLConnection(u, sourceModule) return JpmsResourceURLConnection(u, sourceModule)
} }
} }
@@ -53,7 +57,9 @@ class GbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
@Throws(IOException::class) @Throws(IOException::class)
override fun getInputStream(): InputStream { override fun getInputStream(): InputStream {
return module.getResourceAsStream(getURL().path) val resource = getURL().path
return module.getResourceAsStream(resource)
?: throw ResourceNotFoundException("Resource '$resource' not found in module '${module.name}'")
} }
} }

View File

@@ -24,6 +24,7 @@ import io.netty.handler.codec.memcache.binary.FullBinaryMemcacheRequest
import io.netty.handler.codec.memcache.binary.FullBinaryMemcacheResponse import io.netty.handler.codec.memcache.binary.FullBinaryMemcacheResponse
import io.netty.util.concurrent.GenericFutureListener import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.gbcs.common.ByteBufInputStream import net.woggioni.gbcs.common.ByteBufInputStream
import net.woggioni.gbcs.common.ByteBufOutputStream
import net.woggioni.gbcs.common.GBCS.digest import net.woggioni.gbcs.common.GBCS.digest
import net.woggioni.gbcs.common.HostAndPort import net.woggioni.gbcs.common.HostAndPort
import net.woggioni.gbcs.common.contextLogger import net.woggioni.gbcs.common.contextLogger
@@ -114,13 +115,14 @@ class MemcacheClient(private val cfg: MemcacheCacheConfiguration) : AutoCloseabl
val channel = channelFuture.now val channel = channelFuture.now
val pipeline = channel.pipeline() val pipeline = channel.pipeline()
channel.pipeline() channel.pipeline()
.addLast("handler", object : SimpleChannelInboundHandler<FullBinaryMemcacheResponse>() { .addLast("client-handler", object : SimpleChannelInboundHandler<FullBinaryMemcacheResponse>() {
override fun channelRead0( override fun channelRead0(
ctx: ChannelHandlerContext, ctx: ChannelHandlerContext,
msg: FullBinaryMemcacheResponse msg: FullBinaryMemcacheResponse
) { ) {
pipeline.removeLast() pipeline.removeLast()
pool.release(channel) pool.release(channel)
msg.touch("The method's caller must remember to release this")
response.complete(msg.retain()) response.complete(msg.retain())
} }
@@ -135,6 +137,7 @@ class MemcacheClient(private val cfg: MemcacheCacheConfiguration) : AutoCloseabl
response.completeExceptionally(ex) response.completeExceptionally(ex)
} }
}) })
request.touch()
channel.writeAndFlush(request) channel.writeAndFlush(request)
} else { } else {
response.completeExceptionally(channelFuture.cause()) response.completeExceptionally(channelFuture.cause())
@@ -161,28 +164,35 @@ class MemcacheClient(private val cfg: MemcacheCacheConfiguration) : AutoCloseabl
} }
} }
return sendRequest(request).thenApply { response -> return sendRequest(request).thenApply { response ->
when (val status = response.status()) { try {
BinaryMemcacheResponseStatus.SUCCESS -> { when (val status = response.status()) {
val compressionMode = cfg.compressionMode BinaryMemcacheResponseStatus.SUCCESS -> {
val content = response.content() val compressionMode = cfg.compressionMode
if (compressionMode != null) { val content = response.content().retain()
when (compressionMode) { content.touch()
MemcacheCacheConfiguration.CompressionMode.GZIP -> { if (compressionMode != null) {
GZIPInputStream(ByteBufInputStream(content)) when (compressionMode) {
} MemcacheCacheConfiguration.CompressionMode.GZIP -> {
GZIPInputStream(ByteBufInputStream(content))
}
MemcacheCacheConfiguration.CompressionMode.DEFLATE -> { MemcacheCacheConfiguration.CompressionMode.DEFLATE -> {
InflaterInputStream(ByteBufInputStream(content)) InflaterInputStream(ByteBufInputStream(content))
}
} }
} } else {
} else { ByteBufInputStream(content)
ByteBufInputStream(content) }.let(Channels::newChannel)
}.let(Channels::newChannel) }
BinaryMemcacheResponseStatus.KEY_ENOENT -> {
null
}
else -> throw MemcacheException(status)
} }
BinaryMemcacheResponseStatus.KEY_ENOENT -> { } finally {
null response.release()
}
else -> throw MemcacheException(status)
} }
} }
} }
@@ -197,16 +207,18 @@ class MemcacheClient(private val cfg: MemcacheCacheConfiguration) : AutoCloseabl
extras.writeInt(0) extras.writeInt(0)
extras.writeInt(encodeExpiry(expiry)) extras.writeInt(encodeExpiry(expiry))
val compressionMode = cfg.compressionMode val compressionMode = cfg.compressionMode
content.retain()
val payload = if (compressionMode != null) { val payload = if (compressionMode != null) {
val inputStream = ByteBufInputStream(Unpooled.wrappedBuffer(content)) val inputStream = ByteBufInputStream(content)
val baos = ByteArrayOutputStream() val buf = content.alloc().buffer()
buf.retain()
val outputStream = when (compressionMode) { val outputStream = when (compressionMode) {
MemcacheCacheConfiguration.CompressionMode.GZIP -> { MemcacheCacheConfiguration.CompressionMode.GZIP -> {
GZIPOutputStream(baos) GZIPOutputStream(ByteBufOutputStream(buf))
} }
MemcacheCacheConfiguration.CompressionMode.DEFLATE -> { MemcacheCacheConfiguration.CompressionMode.DEFLATE -> {
DeflaterOutputStream(baos, Deflater(Deflater.DEFAULT_COMPRESSION, false)) DeflaterOutputStream(ByteBufOutputStream(buf), Deflater(Deflater.DEFAULT_COMPRESSION, false))
} }
} }
inputStream.use { i -> inputStream.use { i ->
@@ -214,7 +226,7 @@ class MemcacheClient(private val cfg: MemcacheCacheConfiguration) : AutoCloseabl
JWO.copy(i, o) JWO.copy(i, o)
} }
} }
Unpooled.wrappedBuffer(baos.toByteArray()) buf
} else { } else {
content content
} }
@@ -224,9 +236,13 @@ class MemcacheClient(private val cfg: MemcacheCacheConfiguration) : AutoCloseabl
} }
} }
return sendRequest(request).thenApply { response -> return sendRequest(request).thenApply { response ->
when(val status = response.status()) { try {
BinaryMemcacheResponseStatus.SUCCESS -> null when (val status = response.status()) {
else -> throw MemcacheException(status) BinaryMemcacheResponseStatus.SUCCESS -> null
else -> throw MemcacheException(status)
}
} finally {
response.release()
} }
} }
} }

View File

@@ -1,12 +1,12 @@
package net.woggioni.gbcs.server.cache package net.woggioni.gbcs.server.cache
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import net.woggioni.gbcs.api.Cache import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.common.ByteBufInputStream import net.woggioni.gbcs.common.ByteBufInputStream
import net.woggioni.gbcs.common.ByteBufOutputStream
import net.woggioni.gbcs.common.GBCS.digestString import net.woggioni.gbcs.common.GBCS.digestString
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import java.io.ByteArrayOutputStream
import java.nio.channels.Channels import java.nio.channels.Channels
import java.security.MessageDigest import java.security.MessageDigest
import java.time.Duration import java.time.Duration
@@ -14,6 +14,7 @@ import java.time.Instant
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicLong
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater import java.util.zip.Inflater
@@ -21,11 +22,18 @@ import java.util.zip.InflaterInputStream
class InMemoryCache( class InMemoryCache(
val maxAge: Duration, val maxAge: Duration,
val maxSize: Long,
val digestAlgorithm: String?, val digestAlgorithm: String?,
val compressionEnabled: Boolean, val compressionEnabled: Boolean,
val compressionLevel: Int val compressionLevel: Int
) : Cache { ) : Cache {
companion object {
@JvmStatic
private val log = contextLogger()
}
private val size = AtomicLong()
private val map = ConcurrentHashMap<String, ByteBuf>() private val map = ConcurrentHashMap<String, ByteBuf>()
private class RemovalQueueElement(val key: String, val value : ByteBuf, val expiry : Instant) : Comparable<RemovalQueueElement> { private class RemovalQueueElement(val key: String, val value : ByteBuf, val expiry : Instant) : Comparable<RemovalQueueElement> {
@@ -38,9 +46,17 @@ class InMemoryCache(
private val garbageCollector = Thread { private val garbageCollector = Thread {
while(true) { while(true) {
val el = removalQueue.take() val el = removalQueue.take()
val buf = el.value
val now = Instant.now() val now = Instant.now()
if(now > el.expiry) { if(now > el.expiry) {
map.remove(el.key, el.value) val removed = map.remove(el.key, buf)
if(removed) {
updateSizeAfterRemoval(buf)
//Decrease the reference count for map
buf.release()
}
//Decrease the reference count for removalQueue
buf.release()
} else { } else {
removalQueue.put(el) removalQueue.put(el)
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1))) Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
@@ -50,6 +66,28 @@ class InMemoryCache(
start() start()
} }
private fun removeEldest() : Long {
while(true) {
val el = removalQueue.take()
val buf = el.value
val removed = map.remove(el.key, buf)
//Decrease the reference count for removalQueue
buf.release()
if(removed) {
val newSize = updateSizeAfterRemoval(buf)
//Decrease the reference count for map
buf.release()
return newSize
}
}
}
private fun updateSizeAfterRemoval(removed: ByteBuf) : Long {
return size.updateAndGet { currentSize : Long ->
currentSize - removed.readableBytes()
}
}
override fun close() { override fun close() {
running = false running = false
garbageCollector.join() garbageCollector.join()
@@ -64,11 +102,13 @@ class InMemoryCache(
).let { digest -> ).let { digest ->
map[digest] map[digest]
?.let { value -> ?.let { value ->
val copy = value.retainedDuplicate()
copy.touch("This has to be released by the caller of the cache")
if (compressionEnabled) { if (compressionEnabled) {
val inflater = Inflater() val inflater = Inflater()
Channels.newChannel(InflaterInputStream(ByteBufInputStream(value), inflater)) Channels.newChannel(InflaterInputStream(ByteBufInputStream(copy), inflater))
} else { } else {
Channels.newChannel(ByteBufInputStream(value)) Channels.newChannel(ByteBufInputStream(copy))
} }
} }
}.let { }.let {
@@ -81,18 +121,29 @@ class InMemoryCache(
?.let { md -> ?.let { md ->
digestString(key.toByteArray(), md) digestString(key.toByteArray(), md)
} ?: key).let { digest -> } ?: key).let { digest ->
content.retain()
val value = if (compressionEnabled) { val value = if (compressionEnabled) {
val deflater = Deflater(compressionLevel) val deflater = Deflater(compressionLevel)
val baos = ByteArrayOutputStream() val buf = content.alloc().buffer()
DeflaterOutputStream(baos, deflater).use { stream -> buf.retain()
JWO.copy(ByteBufInputStream(content), stream) DeflaterOutputStream(ByteBufOutputStream(buf), deflater).use { outputStream ->
ByteBufInputStream(content).use { inputStream ->
JWO.copy(inputStream, outputStream)
}
} }
Unpooled.wrappedBuffer(baos.toByteArray()) buf
} else { } else {
content content
} }
map[digest] = value val old = map.put(digest, value)
removalQueue.put(RemovalQueueElement(digest, value, Instant.now().plus(maxAge))) val delta = value.readableBytes() - (old?.readableBytes() ?: 0)
var newSize = size.updateAndGet { currentSize : Long ->
currentSize + delta
}
removalQueue.put(RemovalQueueElement(digest, value.retain(), Instant.now().plus(maxAge)))
while(newSize > maxSize) {
newSize = removeEldest()
}
}.let { }.let {
CompletableFuture.completedFuture<Void>(null) CompletableFuture.completedFuture<Void>(null)
} }

View File

@@ -6,12 +6,14 @@ import java.time.Duration
data class InMemoryCacheConfiguration( data class InMemoryCacheConfiguration(
val maxAge: Duration, val maxAge: Duration,
val maxSize: Long,
val digestAlgorithm : String?, val digestAlgorithm : String?,
val compressionEnabled: Boolean, val compressionEnabled: Boolean,
val compressionLevel: Int, val compressionLevel: Int,
) : Configuration.Cache { ) : Configuration.Cache {
override fun materialize() = InMemoryCache( override fun materialize() = InMemoryCache(
maxAge, maxAge,
maxSize,
digestAlgorithm, digestAlgorithm,
compressionEnabled, compressionEnabled,
compressionLevel compressionLevel

View File

@@ -21,6 +21,9 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
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 maxSize = el.renderAttribute("max-size")
?.let(java.lang.Long::decode)
?: 0x1000000
val enableCompression = el.renderAttribute("enable-compression") val enableCompression = el.renderAttribute("enable-compression")
?.let(String::toBoolean) ?.let(String::toBoolean)
?: true ?: true
@@ -31,6 +34,7 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
return InMemoryCacheConfiguration( return InMemoryCacheConfiguration(
maxAge, maxAge,
maxSize,
digestAlgorithm, digestAlgorithm,
enableCompression, enableCompression,
compressionLevel compressionLevel
@@ -43,6 +47,7 @@ class InMemoryCacheProvider : CacheProvider<InMemoryCacheConfiguration> {
val prefix = doc.lookupPrefix(GBCS.GBCS_NAMESPACE_URI) val prefix = doc.lookupPrefix(GBCS.GBCS_NAMESPACE_URI)
attr("xs:type", "${prefix}:inMemoryCacheType", GBCS.XML_SCHEMA_NAMESPACE_URI) attr("xs:type", "${prefix}:inMemoryCacheType", GBCS.XML_SCHEMA_NAMESPACE_URI)
attr("max-age", maxAge.toString()) attr("max-age", maxAge.toString())
attr("max-size", maxSize.toString())
digestAlgorithm?.let { digestAlgorithm -> digestAlgorithm?.let { digestAlgorithm ->
attr("digest", digestAlgorithm) attr("digest", digestAlgorithm)
} }

View File

@@ -57,16 +57,29 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED) response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED)
} }
ctx.write(response) ctx.write(response)
val content : Any = when (channel) { when (channel) {
is FileChannel -> DefaultFileRegion(channel, 0, channel.size()) is FileChannel -> {
else -> ChunkedNioStream(channel) val content = DefaultFileRegion(channel, 0, channel.size())
} if (keepAlive) {
if (keepAlive) { ctx.write(content)
ctx.write(content) ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate())
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate()) } else {
} else { ctx.writeAndFlush(content)
ctx.writeAndFlush(content) .addListener(ChannelFutureListener.CLOSE)
.addListener(ChannelFutureListener.CLOSE) }
}
else -> {
val content = ChunkedNioStream(channel)
if (keepAlive) {
ctx.write(content).addListener {
content.close()
}
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT.retainedDuplicate())
} else {
ctx.writeAndFlush(content)
.addListener(ChannelFutureListener.CLOSE)
}
}
} }
} else { } else {
log.debug(ctx) { log.debug(ctx) {
@@ -94,7 +107,7 @@ class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
log.debug(ctx) { log.debug(ctx) {
"Added value for key '$key' to build cache" "Added value for key '$key' to build cache"
} }
cache.put(key, msg.content().retain()).thenRun { cache.put(key, msg.content()).thenRun {
val response = DefaultFullHttpResponse( val response = DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.CREATED, msg.protocolVersion(), HttpResponseStatus.CREATED,
Unpooled.copiedBuffer(key.toByteArray()) Unpooled.copiedBuffer(key.toByteArray())

View File

@@ -52,6 +52,7 @@
<xs:complexContent> <xs:complexContent>
<xs:extension base="gbcs:cacheType"> <xs:extension base="gbcs:cacheType">
<xs:attribute name="max-age" type="xs:duration" default="P1D"/> <xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="max-size" type="xs:token" default="0x1000000"/>
<xs:attribute name="digest" type="xs:token" default="MD5"/> <xs:attribute name="digest" type="xs:token" default="MD5"/>
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/> <xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
<xs:attribute name="compression-level" type="xs:byte" default="-1"/> <xs:attribute name="compression-level" type="xs:byte" default="-1"/>

View File

@@ -51,7 +51,8 @@ class NoAuthServerTest : AbstractServerTest() {
maxAge = Duration.ofSeconds(3600 * 24), maxAge = Duration.ofSeconds(3600 * 24),
compressionEnabled = true, compressionEnabled = true,
digestAlgorithm = "MD5", digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION compressionLevel = Deflater.DEFAULT_COMPRESSION,
maxSize = 0x1000000
), ),
null, null,
null, null,

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
gbcs.version = 0.1.2 gbcs.version = 0.1.3
lys.version = 2025.01.31 lys.version = 2025.02.05
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