implemented streaming request/response streaming
added metadata to cache values added cache servlet for comparison
This commit is contained in:
3
rbcs-servlet/Dockerfile
Normal file
3
rbcs-servlet/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM tomcat:jdk21
|
||||
|
||||
COPY ./rbcs-servlet-*.war /usr/local/tomcat/webapps/rbcs-servlet.war
|
33
rbcs-servlet/build.gradle
Normal file
33
rbcs-servlet/build.gradle
Normal file
@@ -0,0 +1,33 @@
|
||||
plugins {
|
||||
alias(catalog.plugins.kotlin.jvm)
|
||||
alias(catalog.plugins.gradle.docker)
|
||||
id 'war'
|
||||
}
|
||||
|
||||
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
|
||||
|
||||
dependencies {
|
||||
compileOnly catalog.jakarta.servlet.api
|
||||
compileOnly catalog.jakarta.enterprise.cdi.api
|
||||
|
||||
implementation catalog.jwo
|
||||
implementation catalog.jakarta.el
|
||||
implementation catalog.jakarta.cdi.el.api
|
||||
implementation catalog.weld.servlet.core
|
||||
implementation catalog.weld.web
|
||||
}
|
||||
|
||||
Provider<Copy> prepareDockerBuild = tasks.register('prepareDockerBuild', Copy) {
|
||||
group = 'docker'
|
||||
into project.layout.buildDirectory.file('docker')
|
||||
from(tasks.war)
|
||||
from(file('Dockerfile'))
|
||||
}
|
||||
|
||||
Provider<DockerBuildImage> dockerBuild = tasks.register('dockerBuildImage', DockerBuildImage) {
|
||||
group = 'docker'
|
||||
dependsOn(prepareDockerBuild)
|
||||
images.add('gitea.woggioni.net/woggioni/rbcs/servlet:latest')
|
||||
images.add("gitea.woggioni.net/woggioni/rbcs/servlet:${version}")
|
||||
}
|
||||
|
140
rbcs-servlet/conf/server.xml
Normal file
140
rbcs-servlet/conf/server.xml
Normal file
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Note: A "Server" is not itself a "Container", so you may not
|
||||
define subcomponents such as "Valves" at this level.
|
||||
Documentation at /docs/config/server.html
|
||||
-->
|
||||
<Server port="8005" shutdown="SHUTDOWN">
|
||||
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
|
||||
<!-- Security listener. Documentation at /docs/config/listeners.html
|
||||
<Listener className="org.apache.catalina.security.SecurityListener" />
|
||||
-->
|
||||
<!-- OpenSSL support using Tomcat Native -->
|
||||
<Listener className="org.apache.catalina.core.AprLifecycleListener" />
|
||||
<!-- OpenSSL support using FFM API from Java 22 -->
|
||||
<!-- <Listener className="org.apache.catalina.core.OpenSSLLifecycleListener" /> -->
|
||||
<!-- Prevent memory leaks due to use of particular java/javax APIs-->
|
||||
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
|
||||
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
|
||||
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
|
||||
|
||||
<!-- Global JNDI resources
|
||||
Documentation at /docs/jndi-resources-howto.html
|
||||
-->
|
||||
<GlobalNamingResources>
|
||||
<!-- Editable user database that can also be used by
|
||||
UserDatabaseRealm to authenticate users
|
||||
-->
|
||||
<Resource name="UserDatabase" auth="Container"
|
||||
type="org.apache.catalina.UserDatabase"
|
||||
description="User database that can be updated and saved"
|
||||
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
|
||||
pathname="conf/tomcat-users.xml" />
|
||||
</GlobalNamingResources>
|
||||
|
||||
<!-- A "Service" is a collection of one or more "Connectors" that share
|
||||
a single "Container" Note: A "Service" is not itself a "Container",
|
||||
so you may not define subcomponents such as "Valves" at this level.
|
||||
Documentation at /docs/config/service.html
|
||||
-->
|
||||
<Service name="Catalina">
|
||||
|
||||
<!--The connectors can use a shared executor, you can define one or more named thread pools-->
|
||||
<!-- <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="150" minSpareThreads="4"/>-->
|
||||
<Executor name="tomcatThreadPool" namePrefix="virtual-exec-" className="org.apache.catalina.core.StandardVirtualThreadExecutor"/>
|
||||
|
||||
|
||||
|
||||
<!-- A "Connector" represents an endpoint by which requests are received
|
||||
and responses are returned. Documentation at :
|
||||
HTTP Connector: /docs/config/http.html
|
||||
AJP Connector: /docs/config/ajp.html
|
||||
Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
|
||||
-->
|
||||
<!-- <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool"-->
|
||||
<!-- connectionTimeout="20000"-->
|
||||
<!-- redirectPort="8443" />-->
|
||||
<Connector port="8080" protocol="HTTP/1.1"
|
||||
connectionTimeout="20000"
|
||||
redirectPort="8443" />
|
||||
<!-- A "Connector" using the shared thread pool-->
|
||||
<!--
|
||||
<Connector executor="tomcatThreadPool"
|
||||
port="8080" protocol="HTTP/1.1"
|
||||
connectionTimeout="20000"
|
||||
redirectPort="8443" />
|
||||
-->
|
||||
<!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
|
||||
This connector uses the NIO implementation. The default
|
||||
SSLImplementation will depend on the presence of the APR/native
|
||||
library and the useOpenSSL attribute of the AprLifecycleListener.
|
||||
Either JSSE or OpenSSL style configuration may be used regardless of
|
||||
the SSLImplementation selected. JSSE style configuration is used below.
|
||||
-->
|
||||
<!--
|
||||
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
|
||||
maxThreads="150" SSLEnabled="true">
|
||||
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
|
||||
<SSLHostConfig>
|
||||
<Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
|
||||
certificateKeystorePassword="changeit" type="RSA" />
|
||||
</SSLHostConfig>
|
||||
</Connector>
|
||||
-->
|
||||
|
||||
<!-- Define an AJP 1.3 Connector on port 8009 -->
|
||||
<!--
|
||||
<Connector protocol="AJP/1.3"
|
||||
address="::1"
|
||||
port="8009"
|
||||
redirectPort="8443" />
|
||||
-->
|
||||
|
||||
<!-- An Engine represents the entry point (within Catalina) that processes
|
||||
every request. The Engine implementation for Tomcat stand alone
|
||||
analyzes the HTTP headers included with the request, and passes them
|
||||
on to the appropriate Host (virtual host).
|
||||
Documentation at /docs/config/engine.html -->
|
||||
|
||||
<!-- You should set jvmRoute to support load-balancing via AJP ie :
|
||||
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
|
||||
-->
|
||||
<Engine name="Catalina" defaultHost="localhost">
|
||||
|
||||
<!--For clustering, please take a look at documentation at:
|
||||
/docs/cluster-howto.html (simple how to)
|
||||
/docs/config/cluster.html (reference documentation) -->
|
||||
<!--
|
||||
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
|
||||
-->
|
||||
|
||||
<!-- Use the LockOutRealm to prevent attempts to guess user passwords
|
||||
via a brute-force attack -->
|
||||
<Realm className="org.apache.catalina.realm.LockOutRealm">
|
||||
<!-- This Realm uses the UserDatabase configured in the global JNDI
|
||||
resources under the key "UserDatabase". Any edits
|
||||
that are performed against this UserDatabase are immediately
|
||||
available for use by the Realm. -->
|
||||
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
|
||||
resourceName="UserDatabase"/>
|
||||
</Realm>
|
||||
|
||||
<Host name="localhost" appBase="webapps"
|
||||
unpackWARs="true" autoDeploy="true">
|
||||
|
||||
<!-- SingleSignOn valve, share authentication between web applications
|
||||
Documentation at: /docs/config/valve.html -->
|
||||
<!--
|
||||
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
|
||||
-->
|
||||
|
||||
<!-- Access log processes all example.
|
||||
Documentation at: /docs/config/valve.html
|
||||
Note: The pattern used is equivalent to using pattern="common" -->
|
||||
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
|
||||
prefix="localhost_access_log" suffix=".txt"
|
||||
pattern="%h %l %u %t "%r" %s %b" />
|
||||
|
||||
</Host>
|
||||
</Engine>
|
||||
</Service>
|
||||
</Server>
|
58
rbcs-servlet/conf/tomcat-users.xml
Normal file
58
rbcs-servlet/conf/tomcat-users.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<tomcat-users xmlns="http://tomcat.apache.org/xml"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
|
||||
version="1.0">
|
||||
<!--
|
||||
By default, no user is included in the "manager-gui" role required
|
||||
to operate the "/manager/html" web application. If you wish to use this app,
|
||||
you must define such a user - the username and password are arbitrary.
|
||||
|
||||
Built-in Tomcat manager roles:
|
||||
- manager-gui - allows access to the HTML GUI and the status pages
|
||||
- manager-script - allows access to the HTTP API and the status pages
|
||||
- manager-jmx - allows access to the JMX proxy and the status pages
|
||||
- manager-status - allows access to the status pages only
|
||||
|
||||
The users below are wrapped in a comment and are therefore ignored. If you
|
||||
wish to configure one or more of these users for use with the manager web
|
||||
application, do not forget to remove the <!.. ..> that surrounds them. You
|
||||
will also need to set the passwords to something appropriate.
|
||||
-->
|
||||
<!--
|
||||
<user username="admin" password="<must-be-changed>" roles="manager-gui"/>
|
||||
<user username="robot" password="<must-be-changed>" roles="manager-script"/>
|
||||
-->
|
||||
<user username="luser" password="password" roles="manager-gui,admin-gui"/>
|
||||
|
||||
<!--
|
||||
The sample user and role entries below are intended for use with the
|
||||
examples web application. They are wrapped in a comment and thus are ignored
|
||||
when reading this file. If you wish to configure these users for use with the
|
||||
examples web application, do not forget to remove the <!.. ..> that surrounds
|
||||
them. You will also need to set the passwords to something appropriate.
|
||||
-->
|
||||
<!--
|
||||
<role rolename="tomcat"/>
|
||||
<role rolename="role1"/>
|
||||
<user username="tomcat" password="<must-be-changed>" roles="tomcat"/>
|
||||
<user username="both" password="<must-be-changed>" roles="tomcat,role1"/>
|
||||
<user username="role1" password="<must-be-changed>" roles="role1"/>
|
||||
-->
|
||||
</tomcat-users>
|
@@ -0,0 +1,169 @@
|
||||
package net.woggioni.rbcs.servlet
|
||||
|
||||
import jakarta.annotation.PreDestroy
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.servlet.annotation.WebServlet
|
||||
import jakarta.servlet.http.HttpServlet
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import net.woggioni.jwo.HttpClient.HttpStatus
|
||||
import net.woggioni.jwo.JWO
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.PriorityBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Logger
|
||||
|
||||
|
||||
private class CacheKey(private val value: ByteArray) {
|
||||
override fun equals(other: Any?) = if (other is CacheKey) {
|
||||
value.contentEquals(other.value)
|
||||
} else false
|
||||
|
||||
override fun hashCode() = value.contentHashCode()
|
||||
}
|
||||
|
||||
|
||||
@ApplicationScoped
|
||||
open class InMemoryServletCache : AutoCloseable {
|
||||
|
||||
private val maxAge= Duration.ofDays(7)
|
||||
private val maxSize = 0x8000000
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private val log = Logger.getLogger(this::class.java.name)
|
||||
}
|
||||
|
||||
private val size = AtomicLong()
|
||||
private val map = ConcurrentHashMap<CacheKey, ByteArray>()
|
||||
|
||||
private class RemovalQueueElement(val key: CacheKey, val value: ByteArray, val expiry: Instant) :
|
||||
Comparable<RemovalQueueElement> {
|
||||
override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry)
|
||||
}
|
||||
|
||||
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>()
|
||||
|
||||
@Volatile
|
||||
private var running = false
|
||||
|
||||
private val garbageCollector = Thread.ofVirtual().name("in-memory-cache-gc").start {
|
||||
while (running) {
|
||||
val el = removalQueue.poll(1, TimeUnit.SECONDS) ?: continue
|
||||
val value = el.value
|
||||
val now = Instant.now()
|
||||
if (now > el.expiry) {
|
||||
val removed = map.remove(el.key, value)
|
||||
if (removed) {
|
||||
updateSizeAfterRemoval(value)
|
||||
}
|
||||
} else {
|
||||
removalQueue.put(el)
|
||||
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeEldest(): Long {
|
||||
while (true) {
|
||||
val el = removalQueue.take()
|
||||
val value = el.value
|
||||
val removed = map.remove(el.key, value)
|
||||
if (removed) {
|
||||
val newSize = updateSizeAfterRemoval(value)
|
||||
return newSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSizeAfterRemoval(removed: ByteArray): Long {
|
||||
return size.updateAndGet { currentSize: Long ->
|
||||
currentSize - removed.size
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
override fun close() {
|
||||
running = false
|
||||
garbageCollector.join()
|
||||
}
|
||||
|
||||
open fun get(key: ByteArray) = map[CacheKey(key)]
|
||||
|
||||
open fun put(
|
||||
key: ByteArray,
|
||||
value: ByteArray,
|
||||
) {
|
||||
val cacheKey = CacheKey(key)
|
||||
val oldSize = map.put(cacheKey, value)?.let { old ->
|
||||
val result = old.size
|
||||
result
|
||||
} ?: 0
|
||||
val delta = value.size - oldSize
|
||||
var newSize = size.updateAndGet { currentSize: Long ->
|
||||
currentSize + delta
|
||||
}
|
||||
removalQueue.put(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
|
||||
while (newSize > maxSize) {
|
||||
newSize = removeEldest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@WebServlet(urlPatterns = ["/cache/*"])
|
||||
class CacheServlet : HttpServlet() {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private val log = Logger.getLogger(this::class.java.name)
|
||||
}
|
||||
|
||||
@Inject
|
||||
private lateinit var cache : InMemoryServletCache
|
||||
|
||||
private fun getKey(req : HttpServletRequest) : String {
|
||||
return Path.of(req.pathInfo).fileName.toString()
|
||||
}
|
||||
|
||||
override fun doPut(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
val baos = ByteArrayOutputStream()
|
||||
baos.use {
|
||||
JWO.copy(req.inputStream, baos)
|
||||
}
|
||||
val key = getKey(req)
|
||||
cache.put(key.toByteArray(Charsets.UTF_8), baos.toByteArray())
|
||||
resp.status = 201
|
||||
resp.setContentLength(0)
|
||||
log.fine {
|
||||
"[${Thread.currentThread().name}] Added value for key $key"
|
||||
}
|
||||
}
|
||||
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
val key = getKey(req)
|
||||
val value = cache.get(key.toByteArray(Charsets.UTF_8))
|
||||
if (value == null) {
|
||||
log.fine {
|
||||
"[${Thread.currentThread().name}] Cache miss for key $key"
|
||||
}
|
||||
resp.status = HttpStatus.NOT_FOUND.code
|
||||
resp.setContentLength(0)
|
||||
} else {
|
||||
log.fine {
|
||||
"[${Thread.currentThread().name}] Cache hit for key $key"
|
||||
}
|
||||
resp.status = HttpStatus.OK.code
|
||||
resp.setContentLength(value.size)
|
||||
ByteArrayInputStream(value).use {
|
||||
JWO.copy(it, resp.outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
rbcs-servlet/src/main/resources/META-INF/beans.xml
Normal file
5
rbcs-servlet/src/main/resources/META-INF/beans.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
|
||||
version="4.0">
|
||||
</beans>
|
7
rbcs-servlet/src/main/resources/META-INF/context.xml
Normal file
7
rbcs-servlet/src/main/resources/META-INF/context.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Context antiJARLocking="true">
|
||||
<Resource name="BeanManager"
|
||||
auth="Container"
|
||||
type="javax.enterprise.inject.spi.BeanManager"
|
||||
factory="org.jboss.weld.resources.ManagerObjectFactory"/>
|
||||
</Context>
|
8
rbcs-servlet/src/main/resources/logging.properties
Normal file
8
rbcs-servlet/src/main/resources/logging.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
handlers = java.util.logging.ConsoleHandler
|
||||
.level=INFO
|
||||
net.woggioni.rbcs.servlet.level=FINEST
|
||||
java.util.logging.ConsoleHandler.level=INFO
|
||||
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
|
||||
java.util.logging.SimpleFormatter.format = %1$tF %1$tT [%4$s] %2$s %5$s %6$s%n
|
||||
org.apache.catalina.core.ContainerBase.[Catalina].level=ALL
|
||||
org.apache.catalina.core.ContainerBase.[Catalina].handlers=java.util.logging.ConsoleHandler
|
8
rbcs-servlet/src/main/webapp/WEB-INF/web.xml
Normal file
8
rbcs-servlet/src/main/webapp/WEB-INF/web.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
|
||||
version="6.0">
|
||||
<listener>
|
||||
<listener-class>org.jboss.weld.module.web.servlet.WeldTerminalListener</listener-class>
|
||||
</listener>
|
||||
</web-app>
|
Reference in New Issue
Block a user