This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 17339b4ddc48393d1c5033e8aadd49652e9cb02a Author: Mark Thomas <ma...@apache.org> AuthorDate: Wed Jul 24 08:01:26 2024 +0100 Add discardRequestsAndResponses to HTTP/2 with a default of false This aligns HTTP/2 with HTTP/1.1 and recycles and re-uses the coyote request and response objects rather than recreating them for every request. --- .../apache/catalina/connector/CoyoteAdapter.java | 19 +++---- java/org/apache/coyote/http2/Http2Protocol.java | 64 ++++++++++++++++++++++ java/org/apache/coyote/http2/Stream.java | 19 ++++++- webapps/docs/changelog.xml | 6 ++ webapps/docs/config/http2.xml | 8 +++ 5 files changed, 103 insertions(+), 13 deletions(-) diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java index 5149e7e0db..aa5818aea0 100644 --- a/java/org/apache/catalina/connector/CoyoteAdapter.java +++ b/java/org/apache/catalina/connector/CoyoteAdapter.java @@ -308,20 +308,19 @@ public class CoyoteAdapter implements Adapter { if (request == null) { // Create objects request = connector.createRequest(req); - response = connector.createResponse(res); - - // Link objects - request.setResponse(response); - response.setRequest(request); - - // Set as notes req.setNote(ADAPTER_NOTES, request); - res.setNote(ADAPTER_NOTES, response); - - // Set query string encoding req.getParameters().setQueryStringCharset(connector.getURICharset()); } + if (response == null) { + response = connector.createResponse(res); + res.setNote(ADAPTER_NOTES, response); + } + + // Link objects + request.setResponse(response); + response.setRequest(request); + if (connector.getXpoweredBy()) { response.addHeader("X-Powered-By", POWERED_BY); } diff --git a/java/org/apache/coyote/http2/Http2Protocol.java b/java/org/apache/coyote/http2/Http2Protocol.java index 8287af0e69..dc9e38eeb3 100644 --- a/java/org/apache/coyote/http2/Http2Protocol.java +++ b/java/org/apache/coyote/http2/Http2Protocol.java @@ -34,6 +34,7 @@ import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; import org.apache.coyote.http11.upgrade.UpgradeProcessorInternal; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.collections.SynchronizedStack; import org.apache.tomcat.util.modeler.Registry; import org.apache.tomcat.util.net.SocketWrapperBase; import org.apache.tomcat.util.res.StringManager; @@ -99,6 +100,19 @@ public class Http2Protocol implements UpgradeProtocol { private RequestGroupInfo global = new RequestGroupInfo(); + /* + * Setting discardRequestsAndResponses can have a significant performance impact. The magnitude of the impact is + * very application dependent but with a simple Spring Boot application[1] returning a short JSON response running + * on markt's desktop in 2024 the difference was 108k req/s with this set to true compared to 124k req/s with this + * set to false. The larger the response and/or the larger the request processing time, the smaller the performance + * impact of this setting. + * + * [1] https://github.com/markt-asf/spring-boot-http2 + */ + private boolean discardRequestsAndResponses = false; + private final SynchronizedStack<Request> recycledRequests = new SynchronizedStack<>(); + private final SynchronizedStack<Response> recycledResponses = new SynchronizedStack<>(); + @Override public String getHttpUpgradeName(boolean isSSLEnabled) { if (isSSLEnabled) { @@ -390,4 +404,54 @@ public class Http2Protocol implements UpgradeProtocol { public RequestGroupInfo getGlobal() { return global; } + + + public boolean getDiscardRequestsAndResponses() { + return discardRequestsAndResponses; + } + + + public void setDiscardRequestsAndResponses(boolean discardRequestsAndResponses) { + this.discardRequestsAndResponses = discardRequestsAndResponses; + } + + + Request popRequest() { + Request request = null; + if (!discardRequestsAndResponses) { + request = recycledRequests.pop(); + } + if (request == null) { + request = new Request(); + } + return request; + } + + + void pushRequest(Request request) { + request.recycle(); + if (!discardRequestsAndResponses) { + recycledRequests.push(request); + } + } + + + Response popResponse() { + Response response = null; + if (!discardRequestsAndResponses) { + response = recycledResponses.pop(); + } + if (response == null) { + response = new Response(); + } + return response; + } + + + void pushResponse(Response response) { + response.recycle(); + if (!discardRequestsAndResponses) { + recycledResponses.push(response); + } + } } diff --git a/java/org/apache/coyote/http2/Stream.java b/java/org/apache/coyote/http2/Stream.java index 0d649f8bfa..6d2a67b969 100644 --- a/java/org/apache/coyote/http2/Stream.java +++ b/java/org/apache/coyote/http2/Stream.java @@ -85,10 +85,10 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { private final Http2UpgradeHandler handler; private final WindowAllocationManager allocationManager = new WindowAllocationManager(this); private final Request coyoteRequest; - private final Response coyoteResponse = new Response(); + private final Response coyoteResponse; private final StreamInputBuffer inputBuffer; private final StreamOutputBuffer streamOutputBuffer = new StreamOutputBuffer(); - private final Http2OutputBuffer http2OutputBuffer = new Http2OutputBuffer(coyoteResponse, streamOutputBuffer); + private final Http2OutputBuffer http2OutputBuffer; private final AtomicBoolean removedFromActiveCount = new AtomicBoolean(false); // State machine would be too much overhead @@ -114,13 +114,22 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { super(handler.getConnectionId(), identifier); this.handler = handler; setWindowSize(handler.getRemoteSettings().getInitialWindowSize()); + Response coyoteResponse = handler.getProtocol().popResponse(); + this.coyoteResponse = coyoteResponse; + http2OutputBuffer = new Http2OutputBuffer(this.coyoteResponse, streamOutputBuffer); + if (coyoteRequest == null) { // HTTP/2 new request - this.coyoteRequest = new Request(); + coyoteRequest = handler.getProtocol().popRequest(); + this.coyoteRequest = coyoteRequest; this.inputBuffer = new StandardStreamInputBuffer(); this.coyoteRequest.setInputBuffer(inputBuffer); } else { // HTTP/1.1 upgrade + /* + * Implementation note. The request passed in is always newly created so it is safe to recycle it for re-use + * in the Stream.recyle() method + */ this.coyoteRequest = coyoteRequest; this.inputBuffer = new SavedRequestStreamInputBuffer((SavedRequestInputFilter) coyoteRequest.getInputBuffer()); @@ -785,6 +794,10 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { remaining = inputByteBuffer.remaining(); } handler.replaceStream(this, new RecycledStream(getConnectionId(), getIdentifier(), state, remaining)); + coyoteRequest.recycle(); + handler.getProtocol().pushRequest(coyoteRequest); + coyoteResponse.recycle(); + handler.getProtocol().pushResponse(coyoteResponse); } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index f42b648a2e..c798172801 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -150,6 +150,12 @@ the Protocol level since the parser configuration depends on the protocol and the parser is, otherwise, stateless. (markt) </scode> + <add> + Align HTTP/2 with HTTP/1.1 and recycle the container internal request + and response processing objects by default. This behaviour can be + controlled via the new <code>discardRequestsAndResponses</code> + attribute on the HTTP/2 upgrade protocol. (markt) + </add> </changelog> </subsection> <subsection name="jdbc-pool"> diff --git a/webapps/docs/config/http2.xml b/webapps/docs/config/http2.xml index f78aa03538..97b4a4b73d 100644 --- a/webapps/docs/config/http2.xml +++ b/webapps/docs/config/http2.xml @@ -80,6 +80,14 @@ <attributes> + <attribute name="discardRequestsAndResponses" required="false"> + <p>A boolean value which can be used to enable or disable the recycling + of the container internal request and response processing objects. If set + to <code>true</code> the request and response objects will be set for + garbage collection after every request, otherwise they will be reused. + If not specified, this attribute is set to <code>false</code>.</p> + </attribute> + <attribute name="initialWindowSize" required="false"> <p>Controls the initial size of the flow control window for streams that Tomcat advertises to clients. If not specified, the default value of --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org