Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-5320-WebSocketHttpClient2

This commit is contained in:
Lachlan Roberts 2020-11-09 15:17:22 +11:00
commit 80a824854b
72 changed files with 3678 additions and 2854 deletions

View File

@ -23,6 +23,15 @@ jetty-10.0.0.beta3 - 21 October 2020
+ 5475 Update to spifly 1.3.2 and asm 9 + 5475 Update to spifly 1.3.2 and asm 9
+ 5480 NPE from WebInfConfiguration.deconfigure during WebAppContext shutdown + 5480 NPE from WebInfConfiguration.deconfigure during WebAppContext shutdown
jetty-9.4.34.v20201102 - 02 November 2020
+ 5320 Using WebSocketClient with jetty-websocket-httpclient.xml in a Jetty
web application causes ClassCastException
+ 5488 jetty-dir.css not found when using JPMS
+ 5498 ServletHolder lifecycle correctness
+ 5521 ResourceCollection NPE in list()
+ 5535 Support regex in SslContextFactory include/exclude of protocols
+ 5555 NPE for servlet with no mapping
jetty-9.4.33.v20201020 - 20 October 2020 jetty-9.4.33.v20201020 - 20 October 2020
+ 5022 Cleanup ServletHandler, specifically with respect to making filter + 5022 Cleanup ServletHandler, specifically with respect to making filter
chains more extensible chains more extensible

View File

@ -21,14 +21,14 @@
<id>generate-xml-files</id> <id>generate-xml-files</id>
<phase>process-resources</phase> <phase>process-resources</phase>
<configuration> <configuration>
<tasks> <target>
<concat destfile="${project.build.directory}/plugin-context.xml"> <concat destfile="${project.build.directory}/plugin-context.xml">
<filelist dir="src/main/templates/" files="plugin-context-header.xml,env-definitions.xml" /> <filelist dir="src/main/templates/" files="plugin-context-header.xml,env-definitions.xml" />
</concat> </concat>
<concat destfile="${project.build.directory}/test-jndi.xml"> <concat destfile="${project.build.directory}/test-jndi.xml">
<filelist dir="src/main/templates/" files="jetty-test-jndi-header.xml,env-definitions.xml" /> <filelist dir="src/main/templates/" files="jetty-test-jndi-header.xml,env-definitions.xml" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
<goals> <goals>
<goal>run</goal> <goal>run</goal>

View File

@ -85,14 +85,14 @@
<id>generate-xml-files</id> <id>generate-xml-files</id>
<phase>process-resources</phase> <phase>process-resources</phase>
<configuration> <configuration>
<tasks> <target>
<concat destfile="${project.build.directory}/plugin-context.xml"> <concat destfile="${project.build.directory}/plugin-context.xml">
<filelist dir="src/main/templates/" files="plugin-context-header.xml,env-definitions.xml" /> <filelist dir="src/main/templates/" files="plugin-context-header.xml,env-definitions.xml" />
</concat> </concat>
<concat destfile="${project.build.directory}/test-spec.xml"> <concat destfile="${project.build.directory}/test-spec.xml">
<filelist dir="src/main/templates/" files="annotations-context-header.xml,env-definitions.xml" /> <filelist dir="src/main/templates/" files="annotations-context-header.xml,env-definitions.xml" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
<goals> <goals>
<goal>run</goal> <goal>run</goal>

View File

@ -88,9 +88,9 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<mkdir dir="${sources-directory}" /> <mkdir dir="${sources-directory}" />
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -1377,9 +1377,12 @@ public class SslBytesServerTest extends SslBytesTest
// Check that we did not spin // Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500); TimeUnit.MILLISECONDS.sleep(500);
assertThat(sslFills.get(), Matchers.lessThan(100)); // The new HttpInput impl tends to call fill and parse more often than the previous one
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
// this runs the parser an goes to the OS more often but requires less rescheduling.
assertThat(sslFills.get(), Matchers.lessThan(150));
assertThat(sslFlushes.get(), Matchers.lessThan(50)); assertThat(sslFlushes.get(), Matchers.lessThan(50));
assertThat(httpParses.get(), Matchers.lessThan(100)); assertThat(httpParses.get(), Matchers.lessThan(150));
assertNull(request.get(5, TimeUnit.SECONDS)); assertNull(request.get(5, TimeUnit.SECONDS));
@ -1399,9 +1402,12 @@ public class SslBytesServerTest extends SslBytesTest
// Check that we did not spin // Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500); TimeUnit.MILLISECONDS.sleep(500);
assertThat(sslFills.get(), Matchers.lessThan(100)); // The new HttpInput impl tends to call fill and parse more often than the previous one
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
// this runs the parser an goes to the OS more often but requires less rescheduling.
assertThat(sslFills.get(), Matchers.lessThan(150));
assertThat(sslFlushes.get(), Matchers.lessThan(50)); assertThat(sslFlushes.get(), Matchers.lessThan(50));
assertThat(httpParses.get(), Matchers.lessThan(100)); assertThat(httpParses.get(), Matchers.lessThan(150));
closeClient(client); closeClient(client);
} }
@ -1596,9 +1602,12 @@ public class SslBytesServerTest extends SslBytesTest
// Check that we did not spin // Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500); TimeUnit.MILLISECONDS.sleep(500);
assertThat(sslFills.get(), Matchers.lessThan(50)); // The new HttpInput impl tends to call fill and parse more often than the previous one
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
// this runs the parser and goes to the OS more often but requires less rescheduling.
assertThat(sslFills.get(), Matchers.lessThan(70));
assertThat(sslFlushes.get(), Matchers.lessThan(20)); assertThat(sslFlushes.get(), Matchers.lessThan(20));
assertThat(httpParses.get(), Matchers.lessThan(50)); assertThat(httpParses.get(), Matchers.lessThan(70));
closeClient(client); closeClient(client);
} }
@ -1743,9 +1752,12 @@ public class SslBytesServerTest extends SslBytesTest
// Check that we did not spin // Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500); TimeUnit.MILLISECONDS.sleep(500);
assertThat(sslFills.get(), Matchers.lessThan(50)); // The new HttpInput impl tends to call fill and parse more often than the previous one
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
// this runs the parser and goes to the OS more often but requires less rescheduling.
assertThat(sslFills.get(), Matchers.lessThan(80));
assertThat(sslFlushes.get(), Matchers.lessThan(20)); assertThat(sslFlushes.get(), Matchers.lessThan(20));
assertThat(httpParses.get(), Matchers.lessThan(100)); assertThat(httpParses.get(), Matchers.lessThan(120));
closeClient(client); closeClient(client);
} }

View File

@ -21,10 +21,10 @@
There are two main concepts on which the Eclipse Jetty standalone server is based: There are two main concepts on which the Eclipse Jetty standalone server is based:
* the xref:og-begin-arch-modules[Jetty _module_ system], that provides the Jetty features * The xref:og-begin-arch-modules[Jetty _module_ system], that provides the Jetty features
* the xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory], that provides a place where you configure the modules, and therefore the features, you need for your web applications * The xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory], that provides a place where you configure the modules, and therefore the features you need for your web applications
After installing Jetty, you want to setup a xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory] where you configure xref:og-begin-arch-modules[Jetty modules]. After installing Jetty, you will want to set up a xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory] where you configure xref:og-begin-arch-modules[Jetty modules].
[[og-begin-arch-modules]] [[og-begin-arch-modules]]
===== Eclipse Jetty Architecture: Modules ===== Eclipse Jetty Architecture: Modules
@ -33,19 +33,19 @@ The Jetty standalone server is made of components that are assembled together, c
A Jetty _module_ is made of one or more components that work together to provide typically one feature, although they may provide more than one feature. A Jetty _module_ is made of one or more components that work together to provide typically one feature, although they may provide more than one feature.
A Jetty module is nothing more than Jetty components assembled together like you would do using Java APIs, just done in a declarative way using configuration files rather than using Java APIs. A Jetty module is nothing more than Jetty components assembled together like you would do using Java APIs, just done in a declarative way using configuration files.
What you can do in Java code to assemble Jetty components, it can be done using Jetty modules. What you can do in Java code to assemble Jetty components can be done using Jetty modules.
A Jetty module may be dependent on other Jetty modules: for example, the `http` Jetty module depends on the `server` Jetty module, that in turn depends on the `threadpool` and `logging` Jetty modules. A Jetty module may be dependent on other Jetty modules: for example, the `http` Jetty module depends on the `server` Jetty module which in turn depends on the `threadpool` and `logging` Jetty modules.
Every feature in a Jetty server is enabled by enabling correspondent Jetty modules. Every feature in a Jetty server is enabled by enabling the corresponding Jetty module(s).
For example, if you enable only the `http` Jetty module, then your Jetty standalone server will only be able to listen to a network port for clear-text HTTP requests. For example, if you enable only the `http` Jetty module, then your Jetty standalone server will only be able to listen to a network port for clear-text HTTP requests.
It will not be able to process secure HTTP (i.e. `https`) requests, it will not be able to process WebSocket, or HTTP/2 or any other protocol because the correspondent modules have not been enabled. It will not be able to process secure HTTP (i.e. `https`) requests, it will not be able to process WebSocket, or HTTP/2 or any other protocol because the correspondent modules have not been enabled.
You can even start a Jetty server _without_ listening on a network port -- for example because you have enabled a custom module you wrote that provides the features you need. You can even start a Jetty server _without_ listening on a network port -- for example because you have enabled a custom module you wrote that provides the features you need.
This allows the Jetty standalone server to be as small as necessary: modules that are not enabled are not loaded, don't waste memory, and you don't risk that client use a module that you did not know was even there. This allows the Jetty standalone server to be as small as necessary: modules that are not enabled are not loaded, don't waste memory, and you don't risk a client using a module that you did not know was even there.
For more detailed information about the Jetty module system, see xref:og-modules[this section]. For more detailed information about the Jetty module system, see xref:og-modules[this section].
@ -60,7 +60,7 @@ This separation between `$JETTY_HOME` and `$JETTY_BASE` allows upgrades without
`$JETTY_HOME` contains the Jetty runtime and libraries and the default configuration, while a `$JETTY_BASE` contains your web applications and any override of the default configuration. `$JETTY_HOME` contains the Jetty runtime and libraries and the default configuration, while a `$JETTY_BASE` contains your web applications and any override of the default configuration.
For example, with the `$JETTY_HOME` installation the default value for the network port for clear-text HTTP is `8080`. For example, with the `$JETTY_HOME` installation the default value for the network port for clear-text HTTP is `8080`.
However, you want that port to be `6060`, for example because you are behind a load balancer that is configured to forward to the backend on port `6060`. However, you want that port to be `6060`, because you are behind a load balancer that is configured to forward to the backend on port `6060`.
Instead, you want to configure the clear-text HTTP port in your `$JETTY_BASE`. Instead, you want to configure the clear-text HTTP port in your `$JETTY_BASE`.
When you upgrade Jetty, you will upgrade only files in `$JETTY_HOME`, and all the configuration in `$JETTY_BASE` will remain unchanged. When you upgrade Jetty, you will upgrade only files in `$JETTY_HOME`, and all the configuration in `$JETTY_BASE` will remain unchanged.

View File

@ -21,5 +21,5 @@
The Eclipse Jetty distribution is available for download from link:https://www.eclipse.org/jetty/download.html[] The Eclipse Jetty distribution is available for download from link:https://www.eclipse.org/jetty/download.html[]
The Eclipse Jetty distribution is available in both `zip` and `gzip` formats; download the one most appropriate for your system, typically `zip` for Windows and `gzip` for other operative systems. The Eclipse Jetty distribution is available in both `zip` and `gzip` formats; download the one most appropriate for your system, typically `zip` for Windows and `gzip` for other operating systems.

View File

@ -30,6 +30,6 @@ The rest of the instructions in this documentation will refer to this location a
IMPORTANT: It is important that *only* stable release versions are used in production environments. IMPORTANT: It is important that *only* stable release versions are used in production environments.
Versions that have been deprecated or are released as Milestones (M), Alpha, Beta or Release Candidates (RC) are *not* suitable for production as they may contain security flaws or incomplete/non-functioning feature sets. Versions that have been deprecated or are released as Milestones (M), Alpha, Beta or Release Candidates (RC) are *not* suitable for production as they may contain security flaws or incomplete/non-functioning feature sets.
If you are new to Jetty, read the xref:og-begin-arch[Jetty architecture short section] to become familiar with the terms used in this document. If you are new to Jetty, you should read the xref:og-begin-arch[Jetty architecture section below] to become familiar with the terms used in this documentation.
Otherwise, you can jump to the xref:og-begin-start[start Jetty section]. Otherwise, you can jump to the xref:og-begin-start[section on starting Jetty].

View File

@ -18,7 +18,11 @@
package org.eclipse.jetty.fcgi.server; package org.eclipse.jetty.fcgi.server;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Queue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -34,8 +38,10 @@ import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpInput;
import org.eclipse.jetty.server.HttpTransport; import org.eclipse.jetty.server.HttpTransport;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -43,6 +49,9 @@ public class HttpChannelOverFCGI extends HttpChannel
{ {
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverFCGI.class); private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverFCGI.class);
private final Queue<HttpInput.Content> _contentQueue = new LinkedList<>();
private final AutoLock _lock = new AutoLock();
private HttpInput.Content _specialContent;
private final HttpFields.Mutable fields = HttpFields.build(); private final HttpFields.Mutable fields = HttpFields.build();
private final Dispatcher dispatcher; private final Dispatcher dispatcher;
private String method; private String method;
@ -57,6 +66,101 @@ public class HttpChannelOverFCGI extends HttpChannel
this.dispatcher = new Dispatcher(connector.getServer().getThreadPool(), this); this.dispatcher = new Dispatcher(connector.getServer().getThreadPool(), this);
} }
@Override
public boolean onContent(HttpInput.Content content)
{
boolean b = super.onContent(content);
Throwable failure;
try (AutoLock l = _lock.lock())
{
failure = _specialContent == null ? null : _specialContent.getError();
if (failure == null)
_contentQueue.offer(content);
}
if (failure != null)
content.failed(failure);
return b;
}
@Override
public boolean needContent()
{
try (AutoLock l = _lock.lock())
{
boolean hasContent = _specialContent != null || !_contentQueue.isEmpty();
if (LOG.isDebugEnabled())
LOG.debug("needContent has content? {}", hasContent);
return hasContent;
}
}
@Override
public HttpInput.Content produceContent()
{
HttpInput.Content content;
try (AutoLock l = _lock.lock())
{
content = _contentQueue.poll();
if (content == null)
content = _specialContent;
}
if (LOG.isDebugEnabled())
LOG.debug("produceContent has produced {}", content);
return content;
}
@Override
public boolean failAllContent(Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("failing all content with {}", (Object)failure);
List<HttpInput.Content> copy;
try (AutoLock l = _lock.lock())
{
copy = new ArrayList<>(_contentQueue);
_contentQueue.clear();
}
copy.forEach(c -> c.failed(failure));
HttpInput.Content lastContent = copy.isEmpty() ? null : copy.get(copy.size() - 1);
boolean atEof = lastContent != null && lastContent.isEof();
if (LOG.isDebugEnabled())
LOG.debug("failed all content, EOF = {}", atEof);
return atEof;
}
@Override
public boolean failed(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("failed " + x);
try (AutoLock l = _lock.lock())
{
Throwable error = _specialContent == null ? null : _specialContent.getError();
if (error != null && error != x)
error.addSuppressed(x);
else
_specialContent = new HttpInput.ErrorContent(x);
}
return getRequest().getHttpInput().onContentProducible();
}
@Override
protected boolean eof()
{
if (LOG.isDebugEnabled())
LOG.debug("received EOF");
try (AutoLock l = _lock.lock())
{
_specialContent = new HttpInput.EofContent();
}
return getRequest().getHttpInput().onContentProducible();
}
protected void header(HttpField field) protected void header(HttpField field)
{ {
String name = field.getName(); String name = field.getName();
@ -127,12 +231,46 @@ public class HttpChannelOverFCGI extends HttpChannel
public boolean onIdleTimeout(Throwable timeout) public boolean onIdleTimeout(Throwable timeout)
{ {
boolean handle = getRequest().getHttpInput().onIdleTimeout(timeout); boolean handle = doOnIdleTimeout(timeout);
if (handle) if (handle)
execute(this); execute(this);
return !handle; return !handle;
} }
private boolean doOnIdleTimeout(Throwable x)
{
boolean neverDispatched = getState().isIdle();
boolean waitingForContent;
HttpInput.Content specialContent;
try (AutoLock l = _lock.lock())
{
waitingForContent = _contentQueue.isEmpty() || _contentQueue.peek().remaining() == 0;
specialContent = _specialContent;
}
if ((waitingForContent || neverDispatched) && specialContent == null)
{
x.addSuppressed(new Throwable("HttpInput idle timeout"));
try (AutoLock l = _lock.lock())
{
_specialContent = new HttpInput.ErrorContent(x);
}
return getRequest().getHttpInput().onContentProducible();
}
return false;
}
@Override
public void recycle()
{
try (AutoLock l = _lock.lock())
{
if (!_contentQueue.isEmpty())
throw new AssertionError("unconsumed content: " + _contentQueue);
_specialContent = null;
}
super.recycle();
}
private static class Dispatcher implements Runnable private static class Dispatcher implements Runnable
{ {
private final AtomicReference<State> state = new AtomicReference<>(State.IDLE); private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);

View File

@ -122,10 +122,10 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/gcloud/\2-\3.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/gcloud/\2-\3.jar" byline="true" />
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" /> <replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@ -135,12 +135,12 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<concat destfile="${project.build.directory}/gcloud-datastore.mod"> <concat destfile="${project.build.directory}/gcloud-datastore.mod">
<fileset file="src/main/config-template/modules/gcloud-datastore.mod" /> <fileset file="src/main/config-template/modules/gcloud-datastore.mod" />
<fileset file="${project.build.directory}/deps.txt" /> <fileset file="${project.build.directory}/deps.txt" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -514,9 +514,9 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<chmod dir="${assembly-directory}/bin" perm="755" includes="**/*.sh" /> <chmod dir="${assembly-directory}/bin" perm="755" includes="**/*.sh" />
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -53,7 +53,7 @@
<dependency> <dependency>
<groupId>com.sun.xml.ws</groupId> <groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId> <artifactId>jaxws-rt</artifactId>
<version>2.3.0.2</version> <version>2.3.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -22,6 +22,7 @@ import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.WritePendingException; import java.nio.channels.WritePendingException;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -235,6 +236,21 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
return state == CloseState.REMOTELY_CLOSED || state == CloseState.CLOSING || state == CloseState.CLOSED; return state == CloseState.REMOTELY_CLOSED || state == CloseState.CLOSING || state == CloseState.CLOSED;
} }
@Override
public boolean failAllData(Throwable x)
{
List<DataEntry> copy;
try (AutoLock l = lock.lock())
{
dataDemand = 0;
copy = new ArrayList<>(dataQueue);
dataQueue.clear();
}
copy.forEach(dataEntry -> dataEntry.callback.failed(x));
DataEntry lastDataEntry = copy.isEmpty() ? null : copy.get(copy.size() - 1);
return lastDataEntry != null && lastDataEntry.frame.isEndStream();
}
public boolean isLocallyClosed() public boolean isLocallyClosed()
{ {
return closeState.get() == CloseState.LOCALLY_CLOSED; return closeState.get() == CloseState.LOCALLY_CLOSED;

View File

@ -216,6 +216,9 @@ public abstract class HTTP2StreamEndPoint implements EndPoint
else else
{ {
entry.succeed(); entry.succeed();
// WebSocket does not have a backpressure API so you must always demand
// the next frame after succeeding the previous one.
stream.demand(1);
} }
return length; return length;
} }

View File

@ -119,6 +119,14 @@ public interface IStream extends Stream, Attachable, Closeable
*/ */
boolean isRemotelyClosed(); boolean isRemotelyClosed();
/**
* Fail all data queued in the stream and reset
* demand to 0.
* @param x the exception to fail the data with.
* @return true if the end of the stream was reached, false otherwise.
*/
boolean failAllData(Throwable x);
/** /**
* @return whether this stream has been reset (locally or remotely) or has been failed * @return whether this stream has been reset (locally or remotely) or has been failed
* @see #isReset() * @see #isReset()

View File

@ -242,7 +242,10 @@ public interface Stream
* @param callback the callback to complete when the bytes of the DATA frame have been consumed * @param callback the callback to complete when the bytes of the DATA frame have been consumed
* @see #onDataDemanded(Stream, DataFrame, Callback) * @see #onDataDemanded(Stream, DataFrame, Callback)
*/ */
public void onData(Stream stream, DataFrame frame, Callback callback); public default void onData(Stream stream, DataFrame frame, Callback callback)
{
callback.succeeded();
}
/** /**
* <p>Callback method invoked when a DATA frame has been demanded.</p> * <p>Callback method invoked when a DATA frame has been demanded.</p>

View File

@ -0,0 +1,26 @@
@startuml
null:
content:
DEMANDING:
EOF:
[*] --> null
null --> DEMANDING : demand()
null --> EOF : eof()
null -left-> null : onTimeout()
DEMANDING --> DEMANDING : demand()
DEMANDING --> content : onContent()\n onTimeout()
DEMANDING --> EOF : eof()
EOF --> EOF : eof()\n onTimeout()
note bottom of content: content1 -> content2 is only\nvalid if content1 is special
note top of content: content -> null only happens\nwhen content is not special
content --> content : onContent()\n onTimeout()
content --> null: take()
content --> EOF: eof()
@enduml

View File

@ -157,7 +157,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
} }
@Override @Override
public void onData(Stream stream, DataFrame frame, Callback callback) public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{ {
getConnection().onData((IStream)stream, frame, callback); getConnection().onData((IStream)stream, frame, callback);
} }

View File

@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.server;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
@ -57,10 +58,12 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
private boolean _expect100Continue; private boolean _expect100Continue;
private boolean _delayedUntilContent; private boolean _delayedUntilContent;
private boolean _useOutputDirectByteBuffers; private boolean _useOutputDirectByteBuffers;
private final ContentDemander _contentDemander;
public HttpChannelOverHTTP2(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransportOverHTTP2 transport) public HttpChannelOverHTTP2(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransportOverHTTP2 transport)
{ {
super(connector, configuration, endPoint, transport); super(connector, configuration, endPoint, transport);
_contentDemander = new ContentDemander();
} }
protected IStream getStream() protected IStream getStream()
@ -131,9 +134,18 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
_delayedUntilContent = getHttpConfiguration().isDelayDispatchUntilContent() && _delayedUntilContent = getHttpConfiguration().isDelayDispatchUntilContent() &&
!endStream && !_expect100Continue && !connect; !endStream && !_expect100Continue && !connect;
// Delay the demand of DATA frames for CONNECT with :protocol. // Delay the demand of DATA frames for CONNECT with :protocol
if (!connect || request.getProtocol() == null) // or for normal requests expecting 100 continue.
getStream().demand(1); if (connect)
{
if (request.getProtocol() == null)
_contentDemander.demand(false);
}
else
{
if (_delayedUntilContent)
_contentDemander.demand(false);
}
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
{ {
@ -204,6 +216,7 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
{ {
_expect100Continue = false; _expect100Continue = false;
_delayedUntilContent = false; _delayedUntilContent = false;
_contentDemander.recycle();
super.recycle(); super.recycle();
getHttpTransport().recycle(); getHttpTransport().recycle();
} }
@ -224,26 +237,16 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
@Override @Override
public Runnable onData(DataFrame frame, Callback callback) public Runnable onData(DataFrame frame, Callback callback)
{ {
return onRequestContent(frame, callback);
}
public Runnable onRequestContent(DataFrame frame, final Callback callback)
{
Stream stream = getStream();
if (stream.isReset())
{
// Consume previously queued content to
// enlarge the session flow control window.
consumeInput();
// Consume immediately this content.
callback.succeeded();
return null;
}
ByteBuffer buffer = frame.getData(); ByteBuffer buffer = frame.getData();
int length = buffer.remaining(); int length = buffer.remaining();
boolean handle = onContent(new HttpInput.Content(buffer) HttpInput.Content content = new HttpInput.Content(buffer)
{ {
@Override
public boolean isEof()
{
return frame.isEndStream();
}
@Override @Override
public void succeeded() public void succeeded()
{ {
@ -261,23 +264,31 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
{ {
return callback.getInvocationType(); return callback.getInvocationType();
} }
}); };
boolean needed = _contentDemander.onContent(content);
boolean handle = onContent(content);
boolean endStream = frame.isEndStream(); boolean endStream = frame.isEndStream();
if (endStream) if (endStream)
{ {
boolean handleContent = onContentComplete(); boolean handleContent = onContentComplete();
// This will generate EOF -> must happen before onContentProducible.
boolean handleRequest = onRequestComplete(); boolean handleRequest = onRequestComplete();
handle |= handleContent | handleRequest; handle |= handleContent | handleRequest;
} }
boolean woken = needed && getRequest().getHttpInput().onContentProducible();
handle |= woken;
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
{ {
LOG.debug("HTTP2 Request #{}/{}: {} bytes of {} content, handle: {}", Stream stream = getStream();
LOG.debug("HTTP2 Request #{}/{}: {} bytes of {} content, woken: {}, needed: {}, handle: {}",
stream.getId(), stream.getId(),
Integer.toHexString(stream.getSession().hashCode()), Integer.toHexString(stream.getSession().hashCode()),
length, length,
endStream ? "last" : "some", endStream ? "last" : "some",
woken,
needed,
handle); handle);
} }
@ -286,6 +297,326 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
return handle || wasDelayed ? this : null; return handle || wasDelayed ? this : null;
} }
/**
* Demanding content is a marker content that is used to remember that a demand was
* registered into the stream. The {@code needed} flag indicates if the demand originated
* from a call to {@link #produceContent()} when false or {@link #needContent()}
* when true, as {@link HttpInput#onContentProducible()} must only be called
* only when {@link #needContent()} was called.
* Instances of this class must never escape the scope of this channel impl,
* so {@link #produceContent()} must never return one.
*/
private static final class DemandingContent extends HttpInput.SpecialContent
{
private final boolean needed;
private DemandingContent(boolean needed)
{
this.needed = needed;
}
}
private static final HttpInput.Content EOF = new HttpInput.EofContent();
private static final HttpInput.Content DEMANDING_NEEDED = new DemandingContent(true);
private static final HttpInput.Content DEMANDING_NOT_NEEDED = new DemandingContent(false);
private class ContentDemander
{
private final AtomicReference<HttpInput.Content> _content = new AtomicReference<>();
public void recycle()
{
if (LOG.isDebugEnabled())
LOG.debug("recycle {}", this);
HttpInput.Content c = _content.getAndSet(null);
if (c != null && !c.isSpecial())
throw new AssertionError("unconsumed content: " + c);
}
public HttpInput.Content poll()
{
while (true)
{
HttpInput.Content c = _content.get();
if (LOG.isDebugEnabled())
LOG.debug("poll, content = {}", c);
if (c == null || c.isSpecial() || _content.compareAndSet(c, c.isEof() ? EOF : null))
{
if (LOG.isDebugEnabled())
LOG.debug("returning current content");
return c;
}
}
}
public boolean demand(boolean needed)
{
while (true)
{
HttpInput.Content c = _content.get();
if (LOG.isDebugEnabled())
LOG.debug("demand({}), content = {}", needed, c);
if (c instanceof DemandingContent)
{
if (needed && !((DemandingContent)c).needed)
{
if (!_content.compareAndSet(c, DEMANDING_NEEDED))
{
if (LOG.isDebugEnabled())
LOG.debug("already demanding but switched needed flag to true");
continue;
}
}
if (LOG.isDebugEnabled())
LOG.debug("already demanding, returning false");
return false;
}
if (c != null)
{
if (LOG.isDebugEnabled())
LOG.debug("content available, returning true");
return true;
}
if (_content.compareAndSet(null, needed ? DEMANDING_NEEDED : DEMANDING_NOT_NEEDED))
{
IStream stream = getStream();
if (stream == null)
{
_content.set(null);
if (LOG.isDebugEnabled())
LOG.debug("no content available, switched to demanding but stream is now null");
return false;
}
if (LOG.isDebugEnabled())
LOG.debug("no content available, demanding stream {}", stream);
stream.demand(1);
c = _content.get();
boolean hasContent = !(c instanceof DemandingContent) && c != null;
if (LOG.isDebugEnabled())
LOG.debug("has content now? {}", hasContent);
return hasContent;
}
}
}
public boolean onContent(HttpInput.Content content)
{
while (true)
{
HttpInput.Content c = _content.get();
if (LOG.isDebugEnabled())
LOG.debug("content delivered by stream: {}, current content: {}", content, c);
if (c instanceof DemandingContent)
{
if (_content.compareAndSet(c, content))
{
boolean needed = ((DemandingContent)c).needed;
if (LOG.isDebugEnabled())
LOG.debug("replacing demand content with {} succeeded; returning {}", content, needed);
return needed;
}
}
else if (c == null)
{
if (!content.isSpecial())
{
// This should never happen, consider as a bug.
content.failed(new IllegalStateException("Non special content without demand : " + content));
return false;
}
if (_content.compareAndSet(null, content))
{
if (LOG.isDebugEnabled())
LOG.debug("replacing null content with {} succeeded", content);
return false;
}
}
else if (c.isEof() && content.isEof() && content.isEmpty())
{
content.succeeded();
return true;
}
else if (content.getError() != null)
{
if (c.getError() != null)
{
if (c.getError() != content.getError())
c.getError().addSuppressed(content.getError());
return true;
}
if (_content.compareAndSet(c, content))
{
c.failed(content.getError());
if (LOG.isDebugEnabled())
LOG.debug("replacing current content with {} succeeded", content);
return true;
}
}
else if (c.getError() != null && content.remaining() == 0)
{
content.succeeded();
return true;
}
else
{
// This should never happen, consider as a bug.
content.failed(new IllegalStateException("Cannot overwrite exiting content " + c + " with " + content));
return false;
}
}
}
public boolean onTimeout(Throwable failure)
{
while (true)
{
HttpInput.Content c = _content.get();
if (LOG.isDebugEnabled())
LOG.debug("onTimeout with current content: {} and failure = {}", c, failure);
if (!(c instanceof DemandingContent))
return false;
if (_content.compareAndSet(c, new HttpInput.ErrorContent(failure)))
{
if (LOG.isDebugEnabled())
LOG.debug("replacing current content with error succeeded");
return true;
}
}
}
public void eof()
{
while (true)
{
HttpInput.Content c = _content.get();
if (LOG.isDebugEnabled())
LOG.debug("eof with current content: {}", c);
if (c instanceof DemandingContent)
{
if (_content.compareAndSet(c, EOF))
{
if (LOG.isDebugEnabled())
LOG.debug("replacing current content with special EOF succeeded");
return;
}
}
else if (c == null)
{
if (_content.compareAndSet(null, EOF))
{
if (LOG.isDebugEnabled())
LOG.debug("replacing null content with special EOF succeeded");
return;
}
}
else if (c.isEof())
{
if (LOG.isDebugEnabled())
LOG.debug("current content already is EOF");
return;
}
else if (c.remaining() == 0)
{
if (_content.compareAndSet(c, EOF))
{
if (LOG.isDebugEnabled())
LOG.debug("replacing current content with special EOF succeeded");
return;
}
}
else
{
// EOF may arrive with HEADERS frame (e.g. a trailer) that is not flow controlled, so we need to wrap the existing content.
// Covered by HttpTrailersTest.testRequestTrailersWithContent.
HttpInput.Content content = new HttpInput.WrappingContent(c, true);
if (_content.compareAndSet(c, content))
{
if (LOG.isDebugEnabled())
LOG.debug("replacing current content with {} succeeded", content);
return;
}
}
}
}
public boolean failContent(Throwable failure)
{
while (true)
{
HttpInput.Content c = _content.get();
if (LOG.isDebugEnabled())
LOG.debug("failing current content: {} with failure: {}", c, failure);
if (c == null)
return false;
if (c.isSpecial())
return c.isEof();
if (_content.compareAndSet(c, null))
{
c.failed(failure);
if (LOG.isDebugEnabled())
LOG.debug("replacing current content with null succeeded");
return false;
}
}
}
@Override
public String toString()
{
return getClass().getSimpleName() + "@" + hashCode() + " _content=" + _content;
}
}
@Override
public boolean needContent()
{
boolean hasContent = _contentDemander.demand(true);
if (LOG.isDebugEnabled())
LOG.debug("needContent has content? {}", hasContent);
return hasContent;
}
@Override
public HttpInput.Content produceContent()
{
HttpInput.Content content = null;
if (_contentDemander.demand(false))
content = _contentDemander.poll();
if (LOG.isDebugEnabled())
LOG.debug("produceContent produced {}", content);
return content;
}
@Override
public boolean failAllContent(Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("failing all content with {}", (Object)failure);
boolean atEof = getStream().failAllData(failure);
atEof |= _contentDemander.failContent(failure);
if (LOG.isDebugEnabled())
LOG.debug("failed all content, reached EOF? {}", atEof);
return atEof;
}
@Override
public boolean failed(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("failed " + x);
_contentDemander.onContent(new HttpInput.ErrorContent(x));
return getRequest().getHttpInput().onContentProducible();
}
@Override
protected boolean eof()
{
_contentDemander.eof();
return false;
}
@Override @Override
public Runnable onTrailer(HeadersFrame frame) public Runnable onTrailer(HeadersFrame frame)
{ {
@ -301,7 +632,10 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
System.lineSeparator(), trailers); System.lineSeparator(), trailers);
} }
// This will generate EOF -> need to call onContentProducible.
boolean handle = onRequestComplete(); boolean handle = onRequestComplete();
boolean woken = getRequest().getHttpInput().onContentProducible();
handle |= woken;
boolean wasDelayed = _delayedUntilContent; boolean wasDelayed = _delayedUntilContent;
_delayedUntilContent = false; _delayedUntilContent = false;
@ -320,25 +654,30 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
final boolean delayed = _delayedUntilContent; final boolean delayed = _delayedUntilContent;
_delayedUntilContent = false; _delayedUntilContent = false;
boolean result = isIdle(); boolean reset = isIdle();
if (result) if (reset)
consumeInput(); consumeInput();
getHttpTransport().onStreamTimeout(failure); getHttpTransport().onStreamTimeout(failure);
if (getRequest().getHttpInput().onIdleTimeout(failure) || delayed)
failure.addSuppressed(new Throwable("HttpInput idle timeout"));
_contentDemander.onTimeout(failure);
boolean needed = getRequest().getHttpInput().onContentProducible();
if (needed || delayed)
{ {
consumer.accept(this::handleWithContext); consumer.accept(this::handleWithContext);
result = false; reset = false;
} }
return result; return reset;
} }
@Override @Override
public Runnable onFailure(Throwable failure, Callback callback) public Runnable onFailure(Throwable failure, Callback callback)
{ {
getHttpTransport().onStreamFailure(failure); getHttpTransport().onStreamFailure(failure);
boolean handle = getRequest().getHttpInput().failed(failure); boolean handle = failed(failure);
consumeInput(); consumeInput();
return new FailureTask(failure, callback, handle); return new FailureTask(failure, callback, handle);
} }

View File

@ -43,7 +43,7 @@
<dependency> <dependency>
<groupId>org.infinispan.protostream</groupId> <groupId>org.infinispan.protostream</groupId>
<artifactId>protostream</artifactId> <artifactId>protostream</artifactId>
<version>4.2.2.Final</version> <version>${infinispan.protostream.version}</version>
<optional>true</optional> <optional>true</optional>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>

View File

@ -45,10 +45,10 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" /> <replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@ -58,12 +58,12 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<concat destfile="${project.build.directory}/infinispan-embedded-query-libs.mod"> <concat destfile="${project.build.directory}/infinispan-embedded-query-libs.mod">
<fileset file="src/main/config-template/modules/sessions/infinispan/embedded/infinispan-embedded-query-libs.mod" /> <fileset file="src/main/config-template/modules/sessions/infinispan/embedded/infinispan-embedded-query-libs.mod" />
<fileset file="${project.build.directory}/deps.txt" /> <fileset file="${project.build.directory}/deps.txt" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -46,10 +46,10 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" /> <replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@ -59,12 +59,12 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<concat destfile="${project.build.directory}/infinispan-embedded-libs.mod"> <concat destfile="${project.build.directory}/infinispan-embedded-libs.mod">
<fileset file="src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-libs.mod" /> <fileset file="src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-libs.mod" />
<fileset file="${project.build.directory}/deps.txt" /> <fileset file="${project.build.directory}/deps.txt" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -45,11 +45,11 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/infinispan/\2-\3-\4.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/infinispan/\2-\3-\4.jar" byline="true" />
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" /> <replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@ -59,12 +59,12 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<concat destfile="${project.build.directory}/infinispan-remote-query-libs.mod"> <concat destfile="${project.build.directory}/infinispan-remote-query-libs.mod">
<fileset file="src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-query-libs.mod" /> <fileset file="src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-query-libs.mod" />
<fileset file="${project.build.directory}/deps.txt" /> <fileset file="${project.build.directory}/deps.txt" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -46,11 +46,11 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/infinispan/\2-\3-\4.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/infinispan/\2-\3-\4.jar" byline="true" />
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" /> <replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@ -60,12 +60,12 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<concat destfile="${project.build.directory}/infinispan-remote-libs.mod"> <concat destfile="${project.build.directory}/infinispan-remote-libs.mod">
<fileset file="src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-libs.mod" /> <fileset file="src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-libs.mod" />
<fileset file="${project.build.directory}/deps.txt" /> <fileset file="${project.build.directory}/deps.txt" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
@ -111,7 +111,7 @@
<dependency> <dependency>
<groupId>org.infinispan.protostream</groupId> <groupId>org.infinispan.protostream</groupId>
<artifactId>protostream</artifactId> <artifactId>protostream</artifactId>
<version>4.2.2.Final</version> <version>${infinispan.protostream.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -46,14 +46,14 @@
<execution> <execution>
<phase>process-resources</phase> <phase>process-resources</phase>
<configuration> <configuration>
<tasks> <target>
<!--delete file="target/classes/META-INF/MANIFEST.MF" /--> <!--delete file="target/classes/META-INF/MANIFEST.MF" /-->
<copy todir="target/classes/jettyhome"> <copy todir="target/classes/jettyhome">
<fileset dir="jettyhome"> <fileset dir="jettyhome">
<exclude name="**/*.log" /> <exclude name="**/*.log" />
</fileset> </fileset>
</copy> </copy>
</tasks> </target>
</configuration> </configuration>
<goals> <goals>
<goal>run</goal> <goal>run</goal>

View File

@ -43,11 +43,11 @@
<execution> <execution>
<phase>process-resources</phase> <phase>process-resources</phase>
<configuration> <configuration>
<tasks> <target>
<copy todir="target/classes/contexts"> <copy todir="target/classes/contexts">
<fileset dir="contexts" /> <fileset dir="contexts" />
</copy> </copy>
</tasks> </target>
</configuration> </configuration>
<goals> <goals>
<goal>run</goal> <goal>run</goal>

View File

@ -12,7 +12,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<properties> <properties>
<osgi-version>3.6.0.v20100517</osgi-version> <osgi-version>3.7.1</osgi-version>
<osgi-services-version>3.2.100.v20100503</osgi-services-version> <osgi-services-version>3.2.100.v20100503</osgi-services-version>
<equinox-http-servlet-version>1.0.0-v20070606</equinox-http-servlet-version> <equinox-http-servlet-version>1.0.0-v20070606</equinox-http-servlet-version>
</properties> </properties>

View File

@ -30,6 +30,7 @@ import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpInput;
import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Response;
@ -62,6 +63,36 @@ public class SpnegoAuthenticatorTest
return null; return null;
} }
@Override
public boolean failed(Throwable x)
{
return false;
}
@Override
protected boolean eof()
{
return false;
}
@Override
public boolean needContent()
{
return false;
}
@Override
public HttpInput.Content produceContent()
{
return null;
}
@Override
public boolean failAllContent(Throwable failure)
{
return false;
}
@Override @Override
protected HttpOutput newHttpOutput() protected HttpOutput newHttpOutput()
{ {
@ -97,6 +128,36 @@ public class SpnegoAuthenticatorTest
return null; return null;
} }
@Override
public boolean failed(Throwable x)
{
return false;
}
@Override
protected boolean eof()
{
return false;
}
@Override
public boolean needContent()
{
return false;
}
@Override
public HttpInput.Content produceContent()
{
return null;
}
@Override
public boolean failAllContent(Throwable failure)
{
return false;
}
@Override @Override
protected HttpOutput newHttpOutput() protected HttpOutput newHttpOutput()
{ {

View File

@ -0,0 +1,354 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Non-blocking {@link ContentProducer} implementation. Calling {@link #nextContent()} will never block
* but will return null when there is no available content.
*/
class AsyncContentProducer implements ContentProducer
{
private static final Logger LOG = LoggerFactory.getLogger(AsyncContentProducer.class);
private final HttpChannel _httpChannel;
private HttpInput.Interceptor _interceptor;
private HttpInput.Content _rawContent;
private HttpInput.Content _transformedContent;
private boolean _error;
private long _firstByteTimeStamp = Long.MIN_VALUE;
private long _rawContentArrived;
AsyncContentProducer(HttpChannel httpChannel)
{
_httpChannel = httpChannel;
}
@Override
public void recycle()
{
if (LOG.isDebugEnabled())
LOG.debug("recycling {}", this);
_interceptor = null;
_rawContent = null;
_transformedContent = null;
_error = false;
_firstByteTimeStamp = Long.MIN_VALUE;
_rawContentArrived = 0L;
}
@Override
public HttpInput.Interceptor getInterceptor()
{
return _interceptor;
}
@Override
public void setInterceptor(HttpInput.Interceptor interceptor)
{
this._interceptor = interceptor;
}
@Override
public int available()
{
HttpInput.Content content = nextTransformedContent();
int available = content == null ? 0 : content.remaining();
if (LOG.isDebugEnabled())
LOG.debug("available = {}", available);
return available;
}
@Override
public boolean hasContent()
{
boolean hasContent = _rawContent != null;
if (LOG.isDebugEnabled())
LOG.debug("hasContent = {}", hasContent);
return hasContent;
}
@Override
public boolean isError()
{
if (LOG.isDebugEnabled())
LOG.debug("isError = {}", _error);
return _error;
}
@Override
public void checkMinDataRate()
{
long minRequestDataRate = _httpChannel.getHttpConfiguration().getMinRequestDataRate();
if (LOG.isDebugEnabled())
LOG.debug("checkMinDataRate [m={},t={}]", minRequestDataRate, _firstByteTimeStamp);
if (minRequestDataRate > 0 && _firstByteTimeStamp != Long.MIN_VALUE)
{
long period = System.nanoTime() - _firstByteTimeStamp;
if (period > 0)
{
long minimumData = minRequestDataRate * TimeUnit.NANOSECONDS.toMillis(period) / TimeUnit.SECONDS.toMillis(1);
if (getRawContentArrived() < minimumData)
{
if (LOG.isDebugEnabled())
LOG.debug("checkMinDataRate check failed");
BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408,
String.format("Request content data rate < %d B/s", minRequestDataRate));
if (_httpChannel.getState().isResponseCommitted())
{
if (LOG.isDebugEnabled())
LOG.debug("checkMinDataRate aborting channel");
_httpChannel.abort(bad);
}
failCurrentContent(bad);
throw bad;
}
}
}
}
@Override
public long getRawContentArrived()
{
if (LOG.isDebugEnabled())
LOG.debug("getRawContentArrived = {}", _rawContentArrived);
return _rawContentArrived;
}
@Override
public boolean consumeAll(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("consumeAll [e={}]", (Object)x);
failCurrentContent(x);
// A specific HttpChannel mechanism must be used as the following code
// does not guarantee that the channel will synchronously deliver all
// content it already contains:
// while (true)
// {
// HttpInput.Content content = _httpChannel.produceContent();
// ...
// }
// as the HttpChannel's produceContent() contract makes no such promise;
// for instance the H2 implementation calls Stream.demand() that may
// deliver the content asynchronously. Tests in StreamResetTest cover this.
boolean atEof = _httpChannel.failAllContent(x);
if (LOG.isDebugEnabled())
LOG.debug("failed all content of http channel; at EOF? {}", atEof);
return atEof;
}
private void failCurrentContent(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("failing currently held content [r={},t={}]", _rawContent, _transformedContent, x);
if (_transformedContent != null && !_transformedContent.isSpecial())
{
if (_transformedContent != _rawContent)
{
_transformedContent.skip(_transformedContent.remaining());
_transformedContent.failed(x);
}
_transformedContent = null;
}
if (_rawContent != null && !_rawContent.isSpecial())
{
_rawContent.skip(_rawContent.remaining());
_rawContent.failed(x);
_rawContent = null;
}
}
@Override
public boolean onContentProducible()
{
if (LOG.isDebugEnabled())
LOG.debug("onContentProducible");
return _httpChannel.getState().onReadReady();
}
@Override
public HttpInput.Content nextContent()
{
HttpInput.Content content = nextTransformedContent();
if (LOG.isDebugEnabled())
LOG.debug("nextContent = {}", content);
if (content != null)
_httpChannel.getState().onReadIdle();
return content;
}
@Override
public void reclaim(HttpInput.Content content)
{
if (LOG.isDebugEnabled())
LOG.debug("reclaim {} [t={}]", content, _transformedContent);
if (_transformedContent == content)
{
content.succeeded();
if (_transformedContent == _rawContent)
_rawContent = null;
_transformedContent = null;
}
}
@Override
public boolean isReady()
{
HttpInput.Content content = nextTransformedContent();
if (content == null)
{
_httpChannel.getState().onReadUnready();
if (_httpChannel.needContent())
{
content = nextTransformedContent();
if (LOG.isDebugEnabled())
LOG.debug("isReady got transformed content after needContent retry {}", content);
if (content != null)
_httpChannel.getState().onContentAdded();
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("isReady has no transformed content after needContent");
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("isReady got transformed content {}", content);
_httpChannel.getState().onContentAdded();
}
boolean ready = content != null;
if (LOG.isDebugEnabled())
LOG.debug("isReady = {}", ready);
return ready;
}
private HttpInput.Content nextTransformedContent()
{
if (LOG.isDebugEnabled())
LOG.debug("nextTransformedContent [r={},t={}]", _rawContent, _transformedContent);
if (_rawContent == null)
{
_rawContent = produceRawContent();
if (_rawContent == null)
return null;
}
if (_transformedContent != null && _transformedContent.isEmpty())
{
if (_transformedContent != _rawContent)
_transformedContent.succeeded();
if (LOG.isDebugEnabled())
LOG.debug("nulling depleted transformed content");
_transformedContent = null;
}
while (_transformedContent == null)
{
if (_rawContent.isSpecial())
{
// TODO does EOF need to be passed to the interceptors?
_error = _rawContent.getError() != null;
if (LOG.isDebugEnabled())
LOG.debug("raw content is special (with error = {}), returning it", _error);
return _rawContent;
}
if (_interceptor != null)
{
if (LOG.isDebugEnabled())
LOG.debug("using interceptor {} to transform raw content", _interceptor);
_transformedContent = _interceptor.readFrom(_rawContent);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("null interceptor, transformed content = raw content");
_transformedContent = _rawContent;
}
if (_transformedContent != null && _transformedContent.isEmpty())
{
if (_transformedContent != _rawContent)
_transformedContent.succeeded();
if (LOG.isDebugEnabled())
LOG.debug("nulling depleted transformed content");
_transformedContent = null;
}
if (_transformedContent == null)
{
if (_rawContent.isEmpty())
{
_rawContent.succeeded();
_rawContent = null;
if (LOG.isDebugEnabled())
LOG.debug("nulling depleted raw content");
_rawContent = produceRawContent();
if (_rawContent == null)
{
if (LOG.isDebugEnabled())
LOG.debug("produced null raw content, returning null");
return null;
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("raw content is not empty");
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("transformed content is not empty");
}
}
if (LOG.isDebugEnabled())
LOG.debug("returning transformed content {}", _transformedContent);
return _transformedContent;
}
private HttpInput.Content produceRawContent()
{
HttpInput.Content content = _httpChannel.produceContent();
if (content != null)
{
_rawContentArrived += content.remaining();
if (_firstByteTimeStamp == Long.MIN_VALUE)
_firstByteTimeStamp = System.nanoTime();
if (LOG.isDebugEnabled())
LOG.debug("produceRawContent updated rawContentArrived to {} and firstByteTimeStamp to {}", _rawContentArrived, _firstByteTimeStamp);
}
if (LOG.isDebugEnabled())
LOG.debug("produceRawContent produced {}", content);
return content;
}
}

View File

@ -0,0 +1,164 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.util.concurrent.Semaphore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Blocking implementation of {@link ContentProducer}. Calling {@link #nextContent()} will block when
* there is no available content but will never return null.
*/
class BlockingContentProducer implements ContentProducer
{
private static final Logger LOG = LoggerFactory.getLogger(BlockingContentProducer.class);
private final Semaphore _semaphore = new Semaphore(0);
private final AsyncContentProducer _asyncContentProducer;
BlockingContentProducer(AsyncContentProducer delegate)
{
_asyncContentProducer = delegate;
}
@Override
public void recycle()
{
if (LOG.isDebugEnabled())
LOG.debug("recycling {}", this);
_asyncContentProducer.recycle();
_semaphore.drainPermits();
}
@Override
public int available()
{
return _asyncContentProducer.available();
}
@Override
public boolean hasContent()
{
return _asyncContentProducer.hasContent();
}
@Override
public boolean isError()
{
return _asyncContentProducer.isError();
}
@Override
public void checkMinDataRate()
{
_asyncContentProducer.checkMinDataRate();
}
@Override
public long getRawContentArrived()
{
return _asyncContentProducer.getRawContentArrived();
}
@Override
public boolean consumeAll(Throwable x)
{
return _asyncContentProducer.consumeAll(x);
}
@Override
public HttpInput.Content nextContent()
{
while (true)
{
HttpInput.Content content = _asyncContentProducer.nextContent();
if (LOG.isDebugEnabled())
LOG.debug("nextContent async producer returned {}", content);
if (content != null)
return content;
// IFF isReady() returns false then HttpChannel.needContent() has been called,
// thus we know that eventually a call to onContentProducible will come.
if (_asyncContentProducer.isReady())
{
if (LOG.isDebugEnabled())
LOG.debug("nextContent async producer is ready, retrying");
continue;
}
if (LOG.isDebugEnabled())
LOG.debug("nextContent async producer is not ready, waiting on semaphore {}", _semaphore);
try
{
_semaphore.acquire();
}
catch (InterruptedException e)
{
return new HttpInput.ErrorContent(e);
}
}
}
@Override
public void reclaim(HttpInput.Content content)
{
_asyncContentProducer.reclaim(content);
}
@Override
public boolean isReady()
{
boolean ready = available() > 0;
if (LOG.isDebugEnabled())
LOG.debug("isReady = {}", ready);
return ready;
}
@Override
public HttpInput.Interceptor getInterceptor()
{
return _asyncContentProducer.getInterceptor();
}
@Override
public void setInterceptor(HttpInput.Interceptor interceptor)
{
_asyncContentProducer.setInterceptor(interceptor);
}
@Override
public boolean onContentProducible()
{
// In blocking mode, the dispatched thread normally does not have to be rescheduled as it is normally in state
// DISPATCHED blocked on the semaphore that just needs to be released for the dispatched thread to resume. This is why
// this method always returns false.
// But async errors can occur while the dispatched thread is NOT blocked reading (i.e.: in state WAITING),
// so the WAITING to WOKEN transition must be done by the error-notifying thread which then has to reschedule the
// dispatched thread after HttpChannelState.asyncError() is called.
// Calling _asyncContentProducer.wakeup() changes the channel state from WAITING to WOKEN which would prevent the
// subsequent call to HttpChannelState.asyncError() from rescheduling the thread.
// AsyncServletTest.testStartAsyncThenClientStreamIdleTimeout() tests this.
if (LOG.isDebugEnabled())
LOG.debug("onContentProducible releasing semaphore {}", _semaphore);
_semaphore.release();
return false;
}
}

View File

@ -0,0 +1,141 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
/**
* ContentProducer is the bridge between {@link HttpInput} and {@link HttpChannel}.
* It wraps a {@link HttpChannel} and uses the {@link HttpChannel#needContent()},
* {@link HttpChannel#produceContent()} and {@link HttpChannel#failAllContent(Throwable)}
* methods, tracks the current state of the channel's input by updating the
* {@link HttpChannelState} and provides the necessary mechanism to unblock
* the reader thread when using a blocking implementation or to know if the reader thread
* has to be rescheduled when using an async implementation.
*/
public interface ContentProducer
{
/**
* Reset all internal state and clear any held resources.
*/
void recycle();
/**
* Fail all content currently available in this {@link ContentProducer} instance
* as well as in the underlying {@link HttpChannel}.
*
* This call is always non-blocking.
* Doesn't change state.
* @return true if EOF was reached.
*/
boolean consumeAll(Throwable x);
/**
* Check if the current data rate consumption is above the minimal rate.
* Abort the channel, fail the content currently available and throw a
* BadMessageException(REQUEST_TIMEOUT_408) if the check fails.
*/
void checkMinDataRate();
/**
* Get the byte count produced by the underlying {@link HttpChannel}.
*
* This call is always non-blocking.
* Doesn't change state.
* @return the byte count produced by the underlying {@link HttpChannel}.
*/
long getRawContentArrived();
/**
* Get the byte count that can immediately be read from this
* {@link ContentProducer} instance or the underlying {@link HttpChannel}.
*
* This call is always non-blocking.
* Doesn't change state.
* @return the available byte count.
*/
int available();
/**
* Check if this {@link ContentProducer} instance contains some
* content without querying the underlying {@link HttpChannel}.
*
* This call is always non-blocking.
* Doesn't change state.
* Doesn't query the HttpChannel.
* @return true if this {@link ContentProducer} instance contains content, false otherwise.
*/
boolean hasContent();
/**
* Check if the underlying {@link HttpChannel} reached an error content.
* This call is always non-blocking.
* Doesn't change state.
* Doesn't query the HttpChannel.
* @return true if the underlying {@link HttpChannel} reached an error content, false otherwise.
*/
boolean isError();
/**
* Get the next content that can be read from or that describes the special condition
* that was reached (error, eof).
* This call may or may not block until some content is available, depending on the implementation.
* The returned content is decoded by the interceptor set with {@link #setInterceptor(HttpInput.Interceptor)}
* or left as-is if no intercept is set.
* After this call, state can be either of UNREADY or IDLE.
* @return the next content that can be read from or null if the implementation does not block
* and has no available content.
*/
HttpInput.Content nextContent();
/**
* Free up the content by calling {@link HttpInput.Content#succeeded()} on it
* and updating this instance' internal state.
*/
void reclaim(HttpInput.Content content);
/**
* Check if this {@link ContentProducer} instance has some content that can be read without blocking.
* If there is some, the next call to {@link #nextContent()} will not block.
* If there isn't any and the implementation does not block, this method will trigger a
* {@link javax.servlet.ReadListener} callback once some content is available.
* This call is always non-blocking.
* After this call, state can be either of UNREADY or READY.
* @return true if some content is immediately available, false otherwise.
*/
boolean isReady();
/**
* Get the {@link org.eclipse.jetty.server.HttpInput.Interceptor}.
* @return The {@link org.eclipse.jetty.server.HttpInput.Interceptor}, or null if none set.
*/
HttpInput.Interceptor getInterceptor();
/**
* Set the interceptor.
* @param interceptor The interceptor to use.
*/
void setInterceptor(HttpInput.Interceptor interceptor);
/**
* Wake up the thread that is waiting for the next content.
* After this call, state can be READY.
* @return true if the thread has to be rescheduled, false otherwise.
*/
boolean onContentProducible();
}

View File

@ -64,7 +64,7 @@ import org.slf4j.LoggerFactory;
* HttpParser.RequestHandler callbacks. The completion of the active phase is signalled by a call to * HttpParser.RequestHandler callbacks. The completion of the active phase is signalled by a call to
* HttpTransport.completed(). * HttpTransport.completed().
*/ */
public class HttpChannel implements Runnable, HttpOutput.Interceptor public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor
{ {
public static Listener NOOP_LISTENER = new Listener() {}; public static Listener NOOP_LISTENER = new Listener() {};
private static final Logger LOG = LoggerFactory.getLogger(HttpChannel.class); private static final Logger LOG = LoggerFactory.getLogger(HttpChannel.class);
@ -119,11 +119,53 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
return _state.isSendError(); return _state.isSendError();
} }
protected HttpInput newHttpInput(HttpChannelState state) private HttpInput newHttpInput(HttpChannelState state)
{ {
return new HttpInput(state); return new HttpInput(state);
} }
/**
* Notify the channel that content is needed. If some content is immediately available, true is returned and
* {@link #produceContent()} has to be called and will return a non-null object.
* If no content is immediately available, {@link HttpInput#onContentProducible()} is called once some content arrives
* and {@link #produceContent()} can be called without returning null.
* If a failure happens, then {@link HttpInput#onContentProducible()} will be called and an error content will return the
* error on the next call to {@link #produceContent()}.
* @return true if content is immediately available.
*/
public abstract boolean needContent();
/**
* Produce a {@link HttpInput.Content} object with data currently stored within the channel. The produced content
* can be special (meaning calling {@link HttpInput.Content#isSpecial()} returns true) if the channel reached a special
* state, like EOF or an error.
* Once a special content has been returned, all subsequent calls to this method will always return a special content
* of the same kind and {@link #needContent()} will always return true.
* The returned content is "raw", i.e.: not decoded.
* @return a {@link HttpInput.Content} object if one is immediately available without blocking, null otherwise.
*/
public abstract HttpInput.Content produceContent();
/**
* Fail all content that is currently stored within the channel.
* @param failure the failure to fail the content with.
* @return true if EOF was reached while failing all content, false otherwise.
*/
public abstract boolean failAllContent(Throwable failure);
/**
* Fail the channel's input.
* @param failure the failure.
* @return true if the channel needs to be rescheduled.
*/
public abstract boolean failed(Throwable failure);
/**
* Mark the channel's input as EOF.
* @return true if the channel needs to be rescheduled.
*/
protected abstract boolean eof();
protected HttpOutput newHttpOutput() protected HttpOutput newHttpOutput()
{ {
return new HttpOutput(this); return new HttpOutput(this);
@ -303,19 +345,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_transientListeners.clear(); _transientListeners.clear();
} }
public void onAsyncWaitForContent()
{
}
public void onBlockWaitForContent()
{
}
public void onBlockWaitForContentFailure(Throwable failure)
{
getRequest().getHttpInput().failed(failure);
}
@Override @Override
public void run() public void run()
{ {
@ -445,18 +474,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
throw _state.getAsyncContextEvent().getThrowable(); throw _state.getAsyncContextEvent().getThrowable();
} }
case READ_REGISTER:
{
onAsyncWaitForContent();
break;
}
case READ_PRODUCE:
{
_request.getHttpInput().asyncReadProduce();
break;
}
case READ_CALLBACK: case READ_CALLBACK:
{ {
ContextHandler handler = _state.getContextHandler(); ContextHandler handler = _state.getContextHandler();
@ -706,7 +723,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onContent {} {}", this, content); LOG.debug("onContent {} {}", this, content);
_combinedListener.onRequestContent(_request, content.getByteBuffer()); _combinedListener.onRequestContent(_request, content.getByteBuffer());
return _request.getHttpInput().addContent(content); return false;
} }
public boolean onContentComplete() public boolean onContentComplete()
@ -729,7 +746,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onRequestComplete {}", this); LOG.debug("onRequestComplete {}", this);
boolean result = _request.getHttpInput().eof(); boolean result = eof();
_combinedListener.onRequestEnd(_request); _combinedListener.onRequestEnd(_request);
return result; return result;
} }
@ -765,11 +782,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_transport.onCompleted(); _transport.onCompleted();
} }
public boolean onEarlyEOF()
{
return _request.getHttpInput().earlyEOF();
}
public void onBadMessage(BadMessageException failure) public void onBadMessage(BadMessageException failure)
{ {
int status = failure.getCode(); int status = failure.getCode();

View File

@ -40,6 +40,7 @@ import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -50,6 +51,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
{ {
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverHttp.class); private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverHttp.class);
private static final HttpField PREAMBLE_UPGRADE_H2C = new HttpField(HttpHeader.UPGRADE, "h2c"); private static final HttpField PREAMBLE_UPGRADE_H2C = new HttpField(HttpHeader.UPGRADE, "h2c");
private static final HttpInput.Content EOF = new HttpInput.EofContent();
private final HttpConnection _httpConnection; private final HttpConnection _httpConnection;
private final RequestBuilder _requestBuilder = new RequestBuilder(); private final RequestBuilder _requestBuilder = new RequestBuilder();
private MetaData.Request _metadata; private MetaData.Request _metadata;
@ -61,6 +63,14 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
private boolean _expect102Processing = false; private boolean _expect102Processing = false;
private List<String> _complianceViolations; private List<String> _complianceViolations;
private HttpFields.Mutable _trailers; private HttpFields.Mutable _trailers;
// Field _content doesn't need to be volatile nor protected by a lock
// as it is always accessed by the same thread, i.e.: we get notified by onFillable
// that the socket contains new bytes and either schedule an onDataAvailable
// call that is going to read the socket or release the blocking semaphore to wake up
// the blocked reader and make it read the socket. The same logic is true for async
// events like timeout: we get notified and either schedule onError or release the
// blocking semaphore.
private HttpInput.Content _content;
public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport) public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport)
{ {
@ -75,6 +85,79 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
_httpConnection.getGenerator().setPersistent(false); _httpConnection.getGenerator().setPersistent(false);
} }
@Override
public boolean needContent()
{
if (_content != null)
{
if (LOG.isDebugEnabled())
LOG.debug("needContent has content immediately available: {}", _content);
return true;
}
_httpConnection.parseAndFillForContent();
if (_content != null)
{
if (LOG.isDebugEnabled())
LOG.debug("needContent has content after parseAndFillForContent: {}", _content);
return true;
}
if (LOG.isDebugEnabled())
LOG.debug("needContent has no content");
_httpConnection.asyncReadFillInterested();
return false;
}
@Override
public HttpInput.Content produceContent()
{
if (_content == null)
{
if (LOG.isDebugEnabled())
LOG.debug("produceContent has no content, parsing and filling");
_httpConnection.parseAndFillForContent();
}
HttpInput.Content result = _content;
if (result != null && !result.isSpecial())
_content = result.isEof() ? EOF : null;
if (LOG.isDebugEnabled())
LOG.debug("produceContent produced {}", result);
return result;
}
@Override
public boolean failAllContent(Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("failing all content with {}", (Object)failure);
if (_content != null && !_content.isSpecial())
{
_content.failed(failure);
_content = _content.isEof() ? EOF : null;
if (_content == EOF)
return true;
}
while (true)
{
HttpInput.Content c = produceContent();
if (c == null)
{
if (LOG.isDebugEnabled())
LOG.debug("failed all content, EOF was not reached");
return false;
}
c.skip(c.remaining());
c.failed(failure);
if (c.isSpecial())
{
boolean atEof = c.isEof();
if (LOG.isDebugEnabled())
LOG.debug("failed all content, EOF = {}", atEof);
return atEof;
}
}
}
@Override @Override
public void badMessage(BadMessageException failure) public void badMessage(BadMessageException failure)
{ {
@ -85,7 +168,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
if (_metadata == null) if (_metadata == null)
_metadata = _requestBuilder.build(); _metadata = _requestBuilder.build();
onRequest(_metadata); onRequest(_metadata);
getRequest().getHttpInput().earlyEOF(); markEarlyEOF();
} }
catch (Exception e) catch (Exception e)
{ {
@ -96,12 +179,23 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
} }
@Override @Override
public boolean content(ByteBuffer content) public boolean content(ByteBuffer buffer)
{ {
HttpInput.Content c = _httpConnection.newContent(content); HttpInput.Content content = _httpConnection.newContent(buffer);
boolean handle = onContent(c) || _delayedForContent; if (_content != null)
_delayedForContent = false; {
return handle; if (_content.isSpecial())
content.failed(_content.getError());
else
throw new AssertionError("Cannot overwrite exiting content " + _content + " with " + content);
}
else
{
_content = content;
onContent(_content);
_delayedForContent = false;
}
return true;
} }
@Override @Override
@ -147,12 +241,69 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
_httpConnection.getGenerator().setPersistent(false); _httpConnection.getGenerator().setPersistent(false);
// If we have no request yet, just close // If we have no request yet, just close
if (_metadata == null) if (_metadata == null)
_httpConnection.close();
else if (onEarlyEOF() || _delayedForContent)
{ {
_delayedForContent = false; _httpConnection.close();
handle();
} }
else
{
markEarlyEOF();
if (_delayedForContent)
{
_delayedForContent = false;
handle();
}
}
}
private void markEarlyEOF()
{
if (LOG.isDebugEnabled())
LOG.debug("received early EOF, content = {}", _content);
EofException failure = new EofException("Early EOF");
if (_content != null)
_content.failed(failure);
_content = new HttpInput.ErrorContent(failure);
}
@Override
protected boolean eof()
{
if (LOG.isDebugEnabled())
LOG.debug("received EOF, content = {}", _content);
if (_content == null)
{
_content = EOF;
}
else
{
HttpInput.Content c = _content;
_content = new HttpInput.WrappingContent(c, true);
}
return false;
}
@Override
public boolean failed(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("failed {}, content = {}", x, _content);
Throwable error = null;
if (_content != null && _content.isSpecial())
error = _content.getError();
if (error != null && error != x)
{
error.addSuppressed(x);
}
else
{
if (_content != null)
_content.failed(x);
_content = new HttpInput.ErrorContent(x);
}
return getRequest().getHttpInput().onContentProducible();
} }
@Override @Override
@ -309,24 +460,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
return onRequestComplete(); return onRequestComplete();
} }
@Override
public void onAsyncWaitForContent()
{
_httpConnection.asyncReadFillInterested();
}
@Override
public void onBlockWaitForContent()
{
_httpConnection.blockingReadFillInterested();
}
@Override
public void onBlockWaitForContentFailure(Throwable failure)
{
_httpConnection.blockingReadFailure(failure);
}
@Override @Override
public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String details) public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String details)
{ {
@ -434,6 +567,9 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
_upgrade = null; _upgrade = null;
_trailers = null; _trailers = null;
_metadata = null; _metadata = null;
if (_content != null && !_content.isSpecial())
throw new AssertionError("unconsumed content: " + _content);
_content = null;
} }
@Override @Override
@ -459,12 +595,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
super.handleException(x); super.handleException(x);
} }
@Override
protected HttpInput newHttpInput(HttpChannelState state)
{
return new HttpInputOverHTTP(state);
}
/** /**
* <p>Attempts to perform an HTTP/1.1 upgrade.</p> * <p>Attempts to perform an HTTP/1.1 upgrade.</p>
* <p>The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector * <p>The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector
@ -534,13 +664,24 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
if (_delayedForContent) if (_delayedForContent)
{ {
_delayedForContent = false; _delayedForContent = false;
getRequest().getHttpInput().onIdleTimeout(timeout); doOnIdleTimeout(timeout);
execute(this); execute(this);
return false; return false;
} }
return true; return true;
} }
private void doOnIdleTimeout(Throwable x)
{
boolean neverDispatched = getState().isIdle();
boolean waitingForContent = _content == null || _content.remaining() == 0;
if ((waitingForContent || neverDispatched) && (_content == null || !_content.isSpecial()))
{
x.addSuppressed(new Throwable("HttpInput idle timeout"));
_content = new HttpInput.ErrorContent(x);
}
}
private static class RequestBuilder private static class RequestBuilder
{ {
private final HttpFields.Mutable _fieldsBuilder = HttpFields.build(); private final HttpFields.Mutable _fieldsBuilder = HttpFields.build();

View File

@ -107,12 +107,9 @@ public class HttpChannelState
*/ */
private enum InputState private enum InputState
{ {
IDLE, // No isReady; No data IDLE, // No isReady; No data
REGISTER, // isReady()==false handling; No data UNREADY, // isReady()==false; No data
REGISTERED, // isReady()==false !handling; No data READY // isReady() was false; data is available
POSSIBLE, // isReady()==false async read callback called (http/1 only)
PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only)
READY // isReady() was false, onContentAdded has been called
} }
/* /*
@ -137,8 +134,6 @@ public class HttpChannelState
ASYNC_ERROR, // handle an async error ASYNC_ERROR, // handle an async error
ASYNC_TIMEOUT, // call asyncContext onTimeout ASYNC_TIMEOUT, // call asyncContext onTimeout
WRITE_CALLBACK, // handle an IO write callback WRITE_CALLBACK, // handle an IO write callback
READ_REGISTER, // Register for fill interest
READ_PRODUCE, // Check is a read is possible by parsing/filling
READ_CALLBACK, // handle an IO read callback READ_CALLBACK, // handle an IO read callback
COMPLETE, // Complete the response by closing output COMPLETE, // Complete the response by closing output
TERMINATED, // No further actions TERMINATED, // No further actions
@ -465,19 +460,12 @@ public class HttpChannelState
case ASYNC: case ASYNC:
switch (_inputState) switch (_inputState)
{ {
case POSSIBLE: case IDLE:
_inputState = InputState.PRODUCING; case UNREADY:
return Action.READ_PRODUCE; break;
case READY: case READY:
_inputState = InputState.IDLE; _inputState = InputState.IDLE;
return Action.READ_CALLBACK; return Action.READ_CALLBACK;
case REGISTER:
case PRODUCING:
_inputState = InputState.REGISTERED;
return Action.READ_REGISTER;
case IDLE:
case REGISTERED:
break;
default: default:
throw new IllegalStateException(getStatusStringLocked()); throw new IllegalStateException(getStatusStringLocked());
@ -1222,99 +1210,8 @@ public class HttpChannelState
_channel.getRequest().setAttribute(name, attribute); _channel.getRequest().setAttribute(name, attribute);
} }
/**
* Called to signal async read isReady() has returned false.
* This indicates that there is no content available to be consumed
* and that once the channel enters the ASYNC_WAIT state it will
* register for read interest by calling {@link HttpChannel#onAsyncWaitForContent()}
* either from this method or from a subsequent call to {@link #unhandle()}.
*/
public void onReadUnready()
{
boolean interested = false;
try (AutoLock l = lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onReadUnready {}", toStringLocked());
switch (_inputState)
{
case IDLE:
case READY:
if (_state == State.WAITING)
{
interested = true;
_inputState = InputState.REGISTERED;
}
else
{
_inputState = InputState.REGISTER;
}
break;
case REGISTER:
case REGISTERED:
case POSSIBLE:
case PRODUCING:
break;
default:
throw new IllegalStateException(toStringLocked());
}
}
if (interested)
_channel.onAsyncWaitForContent();
}
/**
* Called to signal that content is now available to read.
* If the channel is in ASYNC_WAIT state and unready (ie isReady() has
* returned false), then the state is changed to ASYNC_WOKEN and true
* is returned.
*
* @return True IFF the channel was unready and in ASYNC_WAIT state
*/
public boolean onContentAdded()
{
boolean woken = false;
try (AutoLock l = lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onContentAdded {}", toStringLocked());
switch (_inputState)
{
case IDLE:
case READY:
break;
case PRODUCING:
_inputState = InputState.READY;
break;
case REGISTER:
case REGISTERED:
_inputState = InputState.READY;
if (_state == State.WAITING)
{
woken = true;
_state = State.WOKEN;
}
break;
default:
throw new IllegalStateException(toStringLocked());
}
}
return woken;
}
/** /**
* Called to signal that the channel is ready for a callback. * Called to signal that the channel is ready for a callback.
* This is similar to calling {@link #onReadUnready()} followed by
* {@link #onContentAdded()}, except that as content is already
* available, read interest is never set.
* *
* @return true if woken * @return true if woken
*/ */
@ -1328,7 +1225,11 @@ public class HttpChannelState
switch (_inputState) switch (_inputState)
{ {
case READY:
_inputState = InputState.READY;
break;
case IDLE: case IDLE:
case UNREADY:
_inputState = InputState.READY; _inputState = InputState.READY;
if (_state == State.WAITING) if (_state == State.WAITING)
{ {
@ -1344,25 +1245,20 @@ public class HttpChannelState
return woken; return woken;
} }
/** public boolean onReadEof()
* Called to indicate that more content may be available,
* but that a handling thread may need to produce (fill/parse)
* it. Typically called by the async read success callback.
*
* @return {@code true} if more content may be available
*/
public boolean onReadPossible()
{ {
boolean woken = false; boolean woken = false;
try (AutoLock l = lock()) try (AutoLock l = lock())
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onReadPossible {}", toStringLocked()); LOG.debug("onReadEof {}", toStringLocked());
switch (_inputState) switch (_inputState)
{ {
case REGISTERED: case IDLE:
_inputState = InputState.POSSIBLE; case READY:
case UNREADY:
_inputState = InputState.READY;
if (_state == State.WAITING) if (_state == State.WAITING)
{ {
woken = true; woken = true;
@ -1377,29 +1273,72 @@ public class HttpChannelState
return woken; return woken;
} }
/** public void onContentAdded()
* Called to signal that a read has read -1.
* Will wake if the read was called while in ASYNC_WAIT state
*
* @return {@code true} if woken
*/
public boolean onReadEof()
{ {
boolean woken = false;
try (AutoLock l = lock()) try (AutoLock l = lock())
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onEof {}", toStringLocked()); LOG.debug("onContentAdded {}", toStringLocked());
// Force read ready so onAllDataRead can be called switch (_inputState)
_inputState = InputState.READY;
if (_state == State.WAITING)
{ {
woken = true; case IDLE:
_state = State.WOKEN; case UNREADY:
case READY:
_inputState = InputState.READY;
break;
default:
throw new IllegalStateException(toStringLocked());
}
}
}
public void onReadIdle()
{
try (AutoLock l = lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onReadIdle {}", toStringLocked());
switch (_inputState)
{
case UNREADY:
case READY:
case IDLE:
_inputState = InputState.IDLE;
break;
default:
throw new IllegalStateException(toStringLocked());
}
}
}
/**
* Called to indicate that more content may be available,
* but that a handling thread may need to produce (fill/parse)
* it. Typically called by the async read success callback.
*/
public void onReadUnready()
{
try (AutoLock l = lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onReadUnready {}", toStringLocked());
switch (_inputState)
{
case IDLE:
case UNREADY:
case READY: // READY->UNREADY is needed by AsyncServletIOTest.testStolenAsyncRead
_inputState = InputState.UNREADY;
break;
default:
throw new IllegalStateException(toStringLocked());
} }
} }
return woken;
} }
public boolean onWritePossible() public boolean onWritePossible()

View File

@ -0,0 +1,84 @@
@startuml
title HttpChannelState
note top of onReadReady_inputState: onReadReady
state "input state" as onReadReady_inputState {
state "IDLE" as onReadReady_IDLE
state "UNREADY" as onReadReady_UNREADY
state "READY" as onReadReady_READY
state "channel state" as onReadReady_channelState {
state "WAITING" as onReadReady_WAITING
state "WOKEN" as onReadReady_WOKEN
onReadReady_WAITING --> onReadReady_WOKEN
}
onReadReady_IDLE --> onReadReady_channelState
onReadReady_UNREADY --> onReadReady_channelState
onReadReady_channelState --> onReadReady_READY
onReadReady_READY --> onReadReady_READY
}
note top of onReadEof_inputState: onReadEof
state "input state" as onReadEof_inputState {
state "IDLE" as onReadEof_IDLE
state "UNREADY" as onReadEof_UNREADY
state "READY" as onReadEof_READY
state "channel state" as onReadEof_channelState {
state "WAITING" as onReadEof_WAITING
state "WOKEN" as onReadEof_WOKEN
onReadEof_WAITING --> onReadEof_WOKEN
}
onReadEof_IDLE --> onReadEof_channelState
onReadEof_UNREADY --> onReadEof_channelState
onReadEof_READY --> onReadEof_channelState
onReadEof_channelState --> onReadEof_READY
}
note top of onReadIdle_inputState: onReadIdle
state "input state" as onReadIdle_inputState {
state "IDLE" as onReadIdle_IDLE
state "UNREADY" as onReadIdle_UNREADY
state "READY" as onReadIdle_READY
onReadIdle_IDLE --> onReadIdle_IDLE
onReadIdle_UNREADY --> onReadIdle_IDLE
onReadIdle_READY --> onReadIdle_IDLE
}
note top of onReadUnready_inputState: onReadUnready
state "input state" as onReadUnready_inputState {
state "IDLE" as onReadUnready_IDLE
state "UNREADY" as onReadUnready_UNREADY
state "READY" as onReadUnready_READY
onReadUnready_IDLE --> onReadUnready_UNREADY
onReadUnready_UNREADY --> onReadUnready_UNREADY
onReadUnready_READY --> onReadUnready_UNREADY
}
note top of onContentAdded_inputState: onContentAdded
state "input state" as onContentAdded_inputState {
state "IDLE" as onContentAdded_IDLE
state "UNREADY" as onContentAdded_UNREADY
state "READY" as onContentAdded_READY
onContentAdded_IDLE --> onContentAdded_READY
onContentAdded_UNREADY --> onContentAdded_READY
onContentAdded_READY --> onContentAdded_READY
}
@enduml

View File

@ -33,7 +33,6 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpParser.RequestHandler;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.AbstractConnection;
@ -68,7 +67,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
private final HttpParser _parser; private final HttpParser _parser;
private final AtomicInteger _contentBufferReferences = new AtomicInteger(); private final AtomicInteger _contentBufferReferences = new AtomicInteger();
private volatile ByteBuffer _requestBuffer = null; private volatile ByteBuffer _requestBuffer = null;
private final BlockingReadCallback _blockingReadCallback = new BlockingReadCallback();
private final AsyncReadCallback _asyncReadCallback = new AsyncReadCallback(); private final AsyncReadCallback _asyncReadCallback = new AsyncReadCallback();
private final SendCallback _sendCallback = new SendCallback(); private final SendCallback _sendCallback = new SendCallback();
private final boolean _recordHttpComplianceViolations; private final boolean _recordHttpComplianceViolations;
@ -316,21 +314,20 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
} }
/** /**
* Fill and parse data looking for content * Parse and fill data, looking for content
*
* @return true if an {@link RequestHandler} method was called and it returned true;
*/ */
protected boolean fillAndParseForContent() void parseAndFillForContent()
{ {
boolean handled = false; // When fillRequestBuffer() is called, it must always be followed by a parseRequestBuffer() call otherwise this method
// doesn't trigger EOF/earlyEOF which breaks AsyncRequestReadTest.testPartialReadThenShutdown()
int filled = Integer.MAX_VALUE;
while (_parser.inContentState()) while (_parser.inContentState())
{ {
int filled = fillRequestBuffer(); boolean handled = parseRequestBuffer();
handled = parseRequestBuffer(); if (handled || filled <= 0)
if (handled || filled <= 0 || _input.hasContent())
break; break;
filled = fillRequestBuffer();
} }
return handled;
} }
private int fillRequestBuffer() private int fillRequestBuffer()
@ -600,25 +597,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
public void asyncReadFillInterested() public void asyncReadFillInterested()
{ {
getEndPoint().fillInterested(_asyncReadCallback); getEndPoint().tryFillInterested(_asyncReadCallback);
}
public void blockingReadFillInterested()
{
// We try fillInterested here because of SSL and
// spurious wakeups. With blocking reads, we read in a loop
// that tries to read/parse content and blocks waiting if there is
// none available. The loop can be woken up by incoming encrypted
// bytes, which due to SSL might not produce any decrypted bytes.
// Thus the loop needs to register fill interest again. However if
// the loop is woken up spuriously, then the register interest again
// can result in a pending read exception, unless we use tryFillInterested.
getEndPoint().tryFillInterested(_blockingReadCallback);
}
public void blockingReadFailure(Throwable e)
{
_blockingReadCallback.failed(e);
} }
@Override @Override
@ -655,8 +634,15 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
@Override @Override
public void succeeded() public void succeeded()
{ {
if (_contentBufferReferences.decrementAndGet() == 0) int counter = _contentBufferReferences.decrementAndGet();
if (counter == 0)
releaseRequestBuffer(); releaseRequestBuffer();
// TODO: this should do something (warn? fail?) if _contentBufferReferences goes below 0
if (counter < 0)
{
LOG.warn("Content reference counting went below zero: {}", counter);
_contentBufferReferences.incrementAndGet();
}
} }
@Override @Override
@ -666,44 +652,30 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
} }
} }
private class BlockingReadCallback implements Callback
{
@Override
public void succeeded()
{
_input.unblock();
}
@Override
public void failed(Throwable x)
{
_input.failed(x);
}
@Override
public InvocationType getInvocationType()
{
// This callback does not block, rather it wakes up the
// thread that is blocked waiting on the read.
return InvocationType.NON_BLOCKING;
}
}
private class AsyncReadCallback implements Callback private class AsyncReadCallback implements Callback
{ {
@Override @Override
public void succeeded() public void succeeded()
{ {
if (_channel.getState().onReadPossible()) if (_channel.getRequest().getHttpInput().onContentProducible())
_channel.handle(); _channel.handle();
} }
@Override @Override
public void failed(Throwable x) public void failed(Throwable x)
{ {
if (_input.failed(x)) if (_channel.failed(x))
_channel.handle(); _channel.handle();
} }
@Override
public InvocationType getInvocationType()
{
// This callback does not block when the HttpInput is in blocking mode,
// rather it wakes up the thread that is blocked waiting on the read;
// but it can if it is in async mode, hence the varying InvocationType.
return _channel.getRequest().getHttpInput().isAsync() ? InvocationType.BLOCKING : InvocationType.NON_BLOCKING;
}
} }
private class SendCallback extends IteratingCallback private class SendCallback extends IteratingCallback

View File

@ -1,35 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.IOException;
public class HttpInputOverHTTP extends HttpInput
{
public HttpInputOverHTTP(HttpChannelState state)
{
super(state);
}
@Override
protected void produceContent() throws IOException
{
((HttpConnection)getHttpChannelState().getHttpChannel().getEndPoint().getConnection()).fillAndParseForContent();
}
}

View File

@ -0,0 +1,16 @@
@startuml
IDLE:
READY:
UNREADY:
[*] --> IDLE
IDLE --> UNREADY : isReady
IDLE -right->READY : isReady
UNREADY -up-> READY : ASYNC onContentProducible
READY -left->IDLE : nextContent
@enduml

View File

@ -0,0 +1,114 @@
@startuml
title "HttpInput"
participant AsyncContentDelivery as "[async\ncontent\ndelivery]"
participant HttpChannel as "Http\nChannel\n"
participant HttpChannelState as "Http\nChannel\nState"
participant HttpInputInterceptor as "Http\nInput.\nInterceptor"
participant AsyncContentProducer as "Async\nContent\nProducer"
participant HttpInput as "Http\nInput\n"
participant Application as "\nApplication\n"
autoactivate on
== Async Read ==
Application->HttpInput: read
activate Application
HttpInput->AsyncContentProducer: nextContent
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
AsyncContentProducer->HttpChannel: produceContent
return raw content or null
alt if raw content is not null
AsyncContentProducer->HttpInputInterceptor: readFrom
return transformed content
end
return
alt if transformed content is not null
AsyncContentProducer->HttpChannelState: onReadIdle
return
end
return content or null
note over HttpInput
throw ISE
if content
is null
end note
HttpInput->AsyncContentProducer: reclaim
return
return
deactivate Application
== isReady ==
Application->HttpInput: isReady
activate Application
HttpInput->AsyncContentProducer: isReady
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
AsyncContentProducer->HttpChannel: produceContent
return raw content or null
alt if raw content is not null
AsyncContentProducer->HttpInputInterceptor: readFrom
return transformed content
end
return
alt if transformed content is not null
AsyncContentProducer->HttpChannelState: onContentAdded
return
else transformed content is null
AsyncContentProducer->HttpChannelState: onReadUnready
return
AsyncContentProducer->HttpChannel: needContent
return
alt if needContent returns true
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
return
alt if transformed content is not null
AsyncContentProducer->HttpChannelState: onContentAdded
return
end
end
end
return boolean\n[transformed\ncontent is not null]
return
deactivate Application
alt if content arrives
AsyncContentDelivery->HttpInput: onContentProducible
HttpInput->AsyncContentProducer: onContentProducible
alt if not at EOF
AsyncContentProducer->HttpChannelState: onReadReady
return true if woken
else if at EOF
AsyncContentProducer->HttpChannelState: onReadEof
return true if woken
end
return true if woken
return true if woken
alt onContentProducible returns true
AsyncContentDelivery->HttpChannel: execute(HttpChannel)
return
end
end
|||
== available ==
Application->HttpInput: available
activate Application
HttpInput->AsyncContentProducer: available
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
AsyncContentProducer->HttpChannel: produceContent
return raw content or null
alt if raw content is not null
AsyncContentProducer->HttpInputInterceptor: readFrom
return transformed content
end
return
return content size or\n0 if content is null
return
deactivate Application
|||
@enduml

View File

@ -0,0 +1,64 @@
@startuml
title "HttpInput"
participant AsyncContentDelivery as "[async\ncontent\ndelivery]"
participant HttpChannel as "Http\nChannel\n"
participant HttpChannelState as "Http\nChannel\nState"
participant AsyncContentProducer as "Async\nContent\nProducer"
participant Semaphore as "\nSemaphore\n"
participant BlockingContentProducer as "Blocking\nContent\nProducer"
participant HttpInput as "Http\nInput\n"
participant Application as "\nApplication\n"
autoactivate on
== Blocking Read ==
Application->HttpInput: read
activate Application
HttpInput->BlockingContentProducer: nextContent
loop
BlockingContentProducer->AsyncContentProducer: nextContent
AsyncContentProducer->AsyncContentProducer: nextTransformedContent
AsyncContentProducer->HttpChannel: produceContent
return
return
alt content is not null
AsyncContentProducer->HttpChannelState: onReadIdle
return
end
return content or null
alt content is null
BlockingContentProducer->HttpChannelState: onReadUnready
return
BlockingContentProducer->HttpChannel: needContent
return
alt needContent returns false
BlockingContentProducer->Semaphore: acquire
return
else needContent returns true
note over BlockingContentProducer
continue loop
end note
end
else content is not null
return non-null content
end
end
' return from BlockingContentProducer: nextContent
HttpInput->BlockingContentProducer: reclaim
BlockingContentProducer->AsyncContentProducer: reclaim
return
return
return
deactivate Application
alt if content arrives
AsyncContentDelivery->HttpInput: wakeup
HttpInput->BlockingContentProducer: wakeup
BlockingContentProducer->Semaphore: release
return
return false
return false
end
@enduml

View File

@ -749,7 +749,7 @@ public class Request implements HttpServletRequest
public long getContentRead() public long getContentRead()
{ {
return _input.getContentConsumed(); return _input.getContentReceived();
} }
@Override @Override

View File

@ -48,6 +48,7 @@ import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.Syntax;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.SessionIdManager; import org.eclipse.jetty.server.SessionIdManager;
@ -645,7 +646,7 @@ public class SessionHandler extends ScopedHandler
HttpCookie cookie = null; HttpCookie cookie = null;
cookie = new HttpCookie( cookie = new HttpCookie(
_cookieConfig.getName(), getSessionCookieName(_cookieConfig),
id, id,
_cookieConfig.getDomain(), _cookieConfig.getDomain(),
sessionPath, sessionPath,
@ -1334,6 +1335,13 @@ public class SessionHandler extends ScopedHandler
public Session getSession(); public Session getSession();
} }
public static String getSessionCookieName(SessionCookieConfig config)
{
if (config == null || config.getName() == null)
return __DefaultSessionCookie;
return config.getName();
}
/** /**
* CookieConfig * CookieConfig
* *
@ -1423,6 +1431,10 @@ public class SessionHandler extends ScopedHandler
{ {
if (_context != null && _context.getContextHandler().isAvailable()) if (_context != null && _context.getContextHandler().isAvailable())
throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
if ("".equals(name))
throw new IllegalArgumentException("Blank cookie name");
if (name != null)
Syntax.requireValidRFC2616Token(name, "Bad Session cookie name");
_sessionCookie = name; _sessionCookie = name;
} }
@ -1596,12 +1608,12 @@ public class SessionHandler extends ScopedHandler
Cookie[] cookies = request.getCookies(); Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) if (cookies != null && cookies.length > 0)
{ {
final String sessionCookie = getSessionCookieConfig().getName(); final String sessionCookie = getSessionCookieName(getSessionCookieConfig());
for (int i = 0; i < cookies.length; i++) for (Cookie cookie : cookies)
{ {
if (sessionCookie.equalsIgnoreCase(cookies[i].getName())) if (sessionCookie.equalsIgnoreCase(cookie.getName()))
{ {
String id = cookies[i].getValue(); String id = cookie.getValue();
requestedSessionIdFromCookie = true; requestedSessionIdFromCookie = true;
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Got Session ID {} from cookie {}", id, sessionCookie); LOG.debug("Got Session ID {} from cookie {}", id, sessionCookie);

View File

@ -0,0 +1,340 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.zip.GZIPOutputStream;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor;
import org.eclipse.jetty.util.compression.CompressionPool;
import org.eclipse.jetty.util.compression.InflaterPool;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class AsyncContentProducerTest
{
private ScheduledExecutorService scheduledExecutorService;
private InflaterPool inflaterPool;
@BeforeEach
public void setUp()
{
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
inflaterPool = new InflaterPool(-1, true);
}
@AfterEach
public void tearDown()
{
scheduledExecutorService.shutdownNow();
}
@Test
public void testAsyncContentProducerNoInterceptor() throws Exception
{
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(buffers);
final String originalContentString = asString(buffers);
CyclicBarrier barrier = new CyclicBarrier(2);
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
assertThat(error, nullValue());
}
@Test
public void testAsyncContentProducerNoInterceptorWithError() throws Exception
{
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(buffers);
final String originalContentString = asString(buffers);
final Throwable expectedError = new EofException("Early EOF");
CyclicBarrier barrier = new CyclicBarrier(2);
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, barrier));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
assertThat(error, Is.is(expectedError));
}
@Test
public void testAsyncContentProducerGzipInterceptor() throws Exception
{
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
final String originalContentString = asString(uncompressedBuffers);
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
CyclicBarrier barrier = new CyclicBarrier(2);
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier));
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
assertThat(error, nullValue());
}
@Test
public void testAsyncContentProducerGzipInterceptorWithTinyBuffers() throws Exception
{
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
final String originalContentString = asString(uncompressedBuffers);
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
CyclicBarrier barrier = new CyclicBarrier(2);
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier));
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 1));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, totalContentBytesCount + buffers.length + 2, 25, 4, barrier);
assertThat(error, nullValue());
}
@Test
public void testBlockingContentProducerGzipInterceptorWithError() throws Exception
{
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
final String originalContentString = asString(uncompressedBuffers);
final Throwable expectedError = new Throwable("HttpInput idle timeout");
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
CyclicBarrier barrier = new CyclicBarrier(2);
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, barrier));
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
assertThat(error, Is.is(expectedError));
}
private Throwable readAndAssertContent(int totalContentBytesCount, String originalContentString, ContentProducer contentProducer, int totalContentCount, int readyCount, int notReadyCount, CyclicBarrier barrier) throws InterruptedException, BrokenBarrierException, TimeoutException
{
int readBytes = 0;
String consumedString = "";
int nextContentCount = 0;
int isReadyFalseCount = 0;
int isReadyTrueCount = 0;
Throwable error = null;
while (true)
{
if (contentProducer.isReady())
isReadyTrueCount++;
else
isReadyFalseCount++;
HttpInput.Content content = contentProducer.nextContent();
nextContentCount++;
if (content == null)
{
barrier.await(5, TimeUnit.SECONDS);
content = contentProducer.nextContent();
nextContentCount++;
}
assertThat(content, notNullValue());
if (content.isSpecial())
{
if (content.isEof())
break;
error = content.getError();
break;
}
byte[] b = new byte[content.remaining()];
readBytes += b.length;
content.getByteBuffer().get(b);
consumedString += new String(b, StandardCharsets.ISO_8859_1);
content.skip(content.remaining());
}
assertThat(nextContentCount, is(totalContentCount));
assertThat(readBytes, is(totalContentBytesCount));
assertThat(consumedString, is(originalContentString));
assertThat(isReadyFalseCount, is(notReadyCount));
assertThat(isReadyTrueCount, is(readyCount));
return error;
}
private static int countRemaining(ByteBuffer[] byteBuffers)
{
int total = 0;
for (ByteBuffer byteBuffer : byteBuffers)
{
total += byteBuffer.remaining();
}
return total;
}
private static String asString(ByteBuffer[] buffers)
{
StringBuilder sb = new StringBuilder();
for (ByteBuffer buffer : buffers)
{
byte[] b = new byte[buffer.remaining()];
buffer.duplicate().get(b);
sb.append(new String(b, StandardCharsets.ISO_8859_1));
}
return sb.toString();
}
private static ByteBuffer gzipByteBuffer(ByteBuffer uncompressedBuffer)
{
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream output = new GZIPOutputStream(baos);
byte[] b = new byte[uncompressedBuffer.remaining()];
uncompressedBuffer.get(b);
output.write(b);
output.close();
return ByteBuffer.wrap(baos.toByteArray());
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private static class ArrayDelayedHttpChannel extends HttpChannel
{
private final ByteBuffer[] byteBuffers;
private final HttpInput.Content finalContent;
private final ScheduledExecutorService scheduledExecutorService;
private final CyclicBarrier barrier;
private int counter;
private volatile HttpInput.Content nextContent;
public ArrayDelayedHttpChannel(ByteBuffer[] byteBuffers, HttpInput.Content finalContent, ScheduledExecutorService scheduledExecutorService, CyclicBarrier barrier)
{
super(new MockConnector(), new HttpConfiguration(), null, null);
this.byteBuffers = new ByteBuffer[byteBuffers.length];
this.finalContent = finalContent;
this.scheduledExecutorService = scheduledExecutorService;
this.barrier = barrier;
for (int i = 0; i < byteBuffers.length; i++)
{
this.byteBuffers[i] = byteBuffers[i].duplicate();
}
}
@Override
public boolean needContent()
{
if (nextContent != null)
return true;
scheduledExecutorService.schedule(() ->
{
if (byteBuffers.length > counter)
nextContent = new HttpInput.Content(byteBuffers[counter++]);
else
nextContent = finalContent;
try
{
barrier.await(5, TimeUnit.SECONDS);
}
catch (Exception e)
{
throw new AssertionError(e);
}
}, 50, TimeUnit.MILLISECONDS);
return false;
}
@Override
public HttpInput.Content produceContent()
{
HttpInput.Content result = nextContent;
nextContent = null;
return result;
}
@Override
public boolean failAllContent(Throwable failure)
{
nextContent = null;
counter = byteBuffers.length;
return false;
}
@Override
public boolean failed(Throwable x)
{
return false;
}
@Override
protected boolean eof()
{
return false;
}
}
}

View File

@ -0,0 +1,320 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPOutputStream;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor;
import org.eclipse.jetty.util.compression.InflaterPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
public class BlockingContentProducerTest
{
private ScheduledExecutorService scheduledExecutorService;
private InflaterPool inflaterPool;
@BeforeEach
public void setUp()
{
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
inflaterPool = new InflaterPool(-1, true);
}
@AfterEach
public void tearDown()
{
scheduledExecutorService.shutdownNow();
}
@Test
public void testBlockingContentProducerNoInterceptor()
{
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(buffers);
final String originalContentString = asString(buffers);
AtomicReference<ContentProducer> ref = new AtomicReference<>();
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible());
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
ref.set(contentProducer);
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
assertThat(error, nullValue());
}
@Test
public void testBlockingContentProducerNoInterceptorWithError()
{
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(buffers);
final String originalContentString = asString(buffers);
final Throwable expectedError = new EofException("Early EOF");
AtomicReference<ContentProducer> ref = new AtomicReference<>();
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, () -> ref.get().onContentProducible());
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
ref.set(contentProducer);
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
assertThat(error, is(expectedError));
}
@Test
public void testBlockingContentProducerGzipInterceptor()
{
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
final String originalContentString = asString(uncompressedBuffers);
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
AtomicReference<ContentProducer> ref = new AtomicReference<>();
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible());
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
ref.set(contentProducer);
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
assertThat(error, nullValue());
}
@Test
public void testBlockingContentProducerGzipInterceptorWithTinyBuffers()
{
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
final String originalContentString = asString(uncompressedBuffers);
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
AtomicReference<ContentProducer> ref = new AtomicReference<>();
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible());
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
ref.set(contentProducer);
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 1));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, totalContentBytesCount + 1, contentProducer);
assertThat(error, nullValue());
}
@Test
public void testBlockingContentProducerGzipInterceptorWithError()
{
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
final String originalContentString = asString(uncompressedBuffers);
final Throwable expectedError = new Throwable("HttpInput idle timeout");
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
AtomicReference<ContentProducer> ref = new AtomicReference<>();
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, () -> ref.get().onContentProducible());
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
ref.set(contentProducer);
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
assertThat(error, is(expectedError));
}
private Throwable readAndAssertContent(int totalContentBytesCount, String originalContentString, int totalContentCount, ContentProducer contentProducer)
{
int readBytes = 0;
int nextContentCount = 0;
String consumedString = "";
Throwable error = null;
while (true)
{
HttpInput.Content content = contentProducer.nextContent();
nextContentCount++;
if (content.isSpecial())
{
if (content.isEof())
break;
error = content.getError();
break;
}
byte[] b = new byte[content.remaining()];
content.getByteBuffer().get(b);
consumedString += new String(b, StandardCharsets.ISO_8859_1);
readBytes += b.length;
}
assertThat(readBytes, is(totalContentBytesCount));
assertThat(nextContentCount, is(totalContentCount));
assertThat(consumedString, is(originalContentString));
return error;
}
private static int countRemaining(ByteBuffer[] byteBuffers)
{
int total = 0;
for (ByteBuffer byteBuffer : byteBuffers)
{
total += byteBuffer.remaining();
}
return total;
}
private static String asString(ByteBuffer[] buffers)
{
StringBuilder sb = new StringBuilder();
for (ByteBuffer buffer : buffers)
{
byte[] b = new byte[buffer.remaining()];
buffer.duplicate().get(b);
sb.append(new String(b, StandardCharsets.ISO_8859_1));
}
return sb.toString();
}
private static ByteBuffer gzipByteBuffer(ByteBuffer uncompressedBuffer)
{
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream output = new GZIPOutputStream(baos);
byte[] b = new byte[uncompressedBuffer.remaining()];
uncompressedBuffer.get(b);
output.write(b);
output.close();
return ByteBuffer.wrap(baos.toByteArray());
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private interface ContentListener
{
void onContent();
}
private static class ArrayDelayedHttpChannel extends HttpChannel
{
private final ByteBuffer[] byteBuffers;
private final HttpInput.Content finalContent;
private final ScheduledExecutorService scheduledExecutorService;
private final ContentListener contentListener;
private int counter;
private volatile HttpInput.Content nextContent;
public ArrayDelayedHttpChannel(ByteBuffer[] byteBuffers, HttpInput.Content finalContent, ScheduledExecutorService scheduledExecutorService, ContentListener contentListener)
{
super(new MockConnector(), new HttpConfiguration(), null, null);
this.byteBuffers = new ByteBuffer[byteBuffers.length];
this.finalContent = finalContent;
this.scheduledExecutorService = scheduledExecutorService;
this.contentListener = contentListener;
for (int i = 0; i < byteBuffers.length; i++)
{
this.byteBuffers[i] = byteBuffers[i].duplicate();
}
}
@Override
public boolean needContent()
{
if (nextContent != null)
return true;
scheduledExecutorService.schedule(() ->
{
if (byteBuffers.length > counter)
nextContent = new HttpInput.Content(byteBuffers[counter++]);
else
nextContent = finalContent;
contentListener.onContent();
}, 50, TimeUnit.MILLISECONDS);
return false;
}
@Override
public HttpInput.Content produceContent()
{
HttpInput.Content result = nextContent;
nextContent = null;
return result;
}
@Override
public boolean failAllContent(Throwable failure)
{
nextContent = null;
counter = byteBuffers.length;
return false;
}
@Override
public boolean failed(Throwable x)
{
return false;
}
@Override
protected boolean eof()
{
return false;
}
}
}

View File

@ -1,735 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.servlet.ReadListener;
import org.eclipse.jetty.server.HttpChannelState.Action;
import org.eclipse.jetty.server.HttpInput.Content;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.thread.Scheduler;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.eclipse.jetty.server.HttpInput.EARLY_EOF_CONTENT;
import static org.eclipse.jetty.server.HttpInput.EOF_CONTENT;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* this tests HttpInput and its interaction with HttpChannelState
*/
public class HttpInputAsyncStateTest
{
private static final Queue<String> __history = new LinkedBlockingQueue<>();
private ByteBuffer _expected = BufferUtil.allocate(16 * 1024);
private boolean _eof;
private boolean _noReadInDataAvailable;
private boolean _completeInOnDataAvailable;
private final ReadListener _listener = new ReadListener()
{
@Override
public void onError(Throwable t)
{
__history.add("onError:" + t);
}
@Override
public void onDataAvailable() throws IOException
{
__history.add("onDataAvailable");
if (!_noReadInDataAvailable && readAvailable() && _completeInOnDataAvailable)
{
__history.add("complete");
_state.complete();
}
}
@Override
public void onAllDataRead() throws IOException
{
__history.add("onAllDataRead");
}
};
private HttpInput _in;
HttpChannelState _state;
public static class TContent extends HttpInput.Content
{
public TContent(String content)
{
super(BufferUtil.toBuffer(content));
}
}
@BeforeEach
public void before()
{
_noReadInDataAvailable = false;
_in = new HttpInput(new HttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
{
@Override
public void onAsyncWaitForContent()
{
__history.add("onAsyncWaitForContent");
}
@Override
public Scheduler getScheduler()
{
return null;
}
})
{
@Override
public void onReadUnready()
{
super.onReadUnready();
__history.add("onReadUnready");
}
@Override
public boolean onContentAdded()
{
boolean wake = super.onContentAdded();
__history.add("onReadPossible " + wake);
return wake;
}
@Override
public boolean onReadReady()
{
boolean wake = super.onReadReady();
__history.add("onReadReady " + wake);
return wake;
}
})
{
@Override
public void wake()
{
__history.add("wake");
}
};
_state = _in.getHttpChannelState();
__history.clear();
}
private void check(String... history)
{
if (history == null || history.length == 0)
assertThat(__history, empty());
else
assertThat(__history.toArray(new String[__history.size()]), Matchers.arrayContaining(history));
__history.clear();
}
private void wake()
{
handle(null);
}
private void handle()
{
handle(null);
}
private void handle(Runnable run)
{
Action action = _state.handling();
loop:
while (true)
{
switch (action)
{
case DISPATCH:
if (run == null)
fail("Run is null during DISPATCH");
run.run();
break;
case READ_CALLBACK:
_in.run();
break;
case TERMINATED:
case WAIT:
break loop;
case COMPLETE:
__history.add("COMPLETE");
break;
case READ_REGISTER:
_state.getHttpChannel().onAsyncWaitForContent();
break;
default:
fail("Bad Action: " + action);
}
action = _state.unhandle();
}
}
private void deliver(Content... content)
{
if (content != null)
{
for (Content c : content)
{
if (c == EOF_CONTENT)
{
_in.eof();
_eof = true;
}
else if (c == HttpInput.EARLY_EOF_CONTENT)
{
_in.earlyEOF();
_eof = true;
}
else
{
_in.addContent(c);
BufferUtil.append(_expected, c.getByteBuffer().slice());
}
}
}
}
boolean readAvailable() throws IOException
{
int len = 0;
try
{
while (_in.isReady())
{
int b = _in.read();
if (b < 0)
{
if (len > 0)
__history.add("read " + len);
__history.add("read -1");
assertTrue(BufferUtil.isEmpty(_expected));
assertTrue(_eof);
return true;
}
else
{
len++;
assertFalse(BufferUtil.isEmpty(_expected));
int a = 0xff & _expected.get();
assertThat(b, equalTo(a));
}
}
__history.add("read " + len);
assertTrue(BufferUtil.isEmpty(_expected));
}
catch (IOException e)
{
if (len > 0)
__history.add("read " + len);
__history.add("read " + e);
throw e;
}
return false;
}
@AfterEach
public void after()
{
assertThat(__history.poll(), Matchers.nullValue());
}
@Test
public void testInitialEmptyListenInHandle() throws Exception
{
deliver(EOF_CONTENT);
check();
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadReady false");
});
check("onAllDataRead");
}
@Test
public void testInitialEmptyListenAfterHandle() throws Exception
{
deliver(EOF_CONTENT);
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check("onAllDataRead");
}
@Test
public void testListenInHandleEmpty() throws Exception
{
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadUnready");
});
check("onAsyncWaitForContent");
deliver(EOF_CONTENT);
check("onReadPossible true");
handle();
check("onAllDataRead");
}
@Test
public void testEmptyListenAfterHandle() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
deliver(EOF_CONTENT);
check();
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check("onAllDataRead");
}
@Test
public void testListenAfterHandleEmpty() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onAsyncWaitForContent", "onReadUnready");
deliver(EOF_CONTENT);
check("onReadPossible true");
handle();
check("onAllDataRead");
}
@Test
public void testInitialEarlyEOFListenInHandle() throws Exception
{
deliver(EARLY_EOF_CONTENT);
check();
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadReady false");
});
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testInitialEarlyEOFListenAfterHandle() throws Exception
{
deliver(EARLY_EOF_CONTENT);
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testListenInHandleEarlyEOF() throws Exception
{
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadUnready");
});
check("onAsyncWaitForContent");
deliver(EARLY_EOF_CONTENT);
check("onReadPossible true");
handle();
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testEarlyEOFListenAfterHandle() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
deliver(EARLY_EOF_CONTENT);
check();
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testListenAfterHandleEarlyEOF() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onAsyncWaitForContent", "onReadUnready");
deliver(EARLY_EOF_CONTENT);
check("onReadPossible true");
handle();
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testInitialAllContentListenInHandle() throws Exception
{
deliver(new TContent("Hello"), EOF_CONTENT);
check();
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadReady false");
});
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
}
@Test
public void testInitialAllContentListenAfterHandle() throws Exception
{
deliver(new TContent("Hello"), EOF_CONTENT);
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
}
@Test
public void testListenInHandleAllContent() throws Exception
{
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadUnready");
});
check("onAsyncWaitForContent");
deliver(new TContent("Hello"), EOF_CONTENT);
check("onReadPossible true", "onReadPossible false");
handle();
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
}
@Test
public void testAllContentListenAfterHandle() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
deliver(new TContent("Hello"), EOF_CONTENT);
check();
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
}
@Test
public void testListenAfterHandleAllContent() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onAsyncWaitForContent", "onReadUnready");
deliver(new TContent("Hello"), EOF_CONTENT);
check("onReadPossible true", "onReadPossible false");
handle();
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
}
@Test
public void testInitialIncompleteContentListenInHandle() throws Exception
{
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
check();
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadReady false");
});
check(
"onDataAvailable",
"read 5",
"read org.eclipse.jetty.io.EofException: Early EOF",
"onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testInitialPartialContentListenAfterHandle() throws Exception
{
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check(
"onDataAvailable",
"read 5",
"read org.eclipse.jetty.io.EofException: Early EOF",
"onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testListenInHandlePartialContent() throws Exception
{
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadUnready");
});
check("onAsyncWaitForContent");
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
check("onReadPossible true", "onReadPossible false");
handle();
check(
"onDataAvailable",
"read 5",
"read org.eclipse.jetty.io.EofException: Early EOF",
"onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testPartialContentListenAfterHandle() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
check();
_in.setReadListener(_listener);
check("onReadReady true", "wake");
wake();
check(
"onDataAvailable",
"read 5",
"read org.eclipse.jetty.io.EofException: Early EOF",
"onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testListenAfterHandlePartialContent() throws Exception
{
handle(() ->
{
_state.startAsync(null);
check();
});
_in.setReadListener(_listener);
check("onAsyncWaitForContent", "onReadUnready");
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
check("onReadPossible true", "onReadPossible false");
handle();
check(
"onDataAvailable",
"read 5",
"read org.eclipse.jetty.io.EofException: Early EOF",
"onError:org.eclipse.jetty.io.EofException: Early EOF");
}
@Test
public void testReadAfterOnDataAvailable() throws Exception
{
_noReadInDataAvailable = true;
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadUnready");
});
check("onAsyncWaitForContent");
deliver(new TContent("Hello"), EOF_CONTENT);
check("onReadPossible true", "onReadPossible false");
handle();
check("onDataAvailable");
readAvailable();
check("wake", "read 5", "read -1");
wake();
check("onAllDataRead");
}
@Test
public void testReadOnlyExpectedAfterOnDataAvailable() throws Exception
{
_noReadInDataAvailable = true;
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadUnready");
});
check("onAsyncWaitForContent");
deliver(new TContent("Hello"), EOF_CONTENT);
check("onReadPossible true", "onReadPossible false");
handle();
check("onDataAvailable");
byte[] buffer = new byte[_expected.remaining()];
assertThat(_in.read(buffer), equalTo(buffer.length));
assertThat(new String(buffer), equalTo(BufferUtil.toString(_expected)));
BufferUtil.clear(_expected);
check();
assertTrue(_in.isReady());
check();
assertThat(_in.read(), equalTo(-1));
check("wake");
wake();
check("onAllDataRead");
}
@Test
public void testReadAndCompleteInOnDataAvailable() throws Exception
{
_completeInOnDataAvailable = true;
handle(() ->
{
_state.startAsync(null);
_in.setReadListener(_listener);
check("onReadUnready");
});
check("onAsyncWaitForContent");
deliver(new TContent("Hello"), EOF_CONTENT);
check("onReadPossible true", "onReadPossible false");
handle(() ->
{
__history.add(_state.getState().toString());
});
System.err.println(__history);
check(
"onDataAvailable",
"read 5",
"read -1",
"complete",
"COMPLETE"
);
}
}

View File

@ -1,614 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.EOFException;
import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
import javax.servlet.ReadListener;
import org.eclipse.jetty.util.BufferUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpInputTest
{
private final Queue<String> _history = new LinkedBlockingQueue<>();
private final Queue<String> _fillAndParseSimulate = new LinkedBlockingQueue<>();
private final ReadListener _listener = new ReadListener()
{
@Override
public void onError(Throwable t)
{
_history.add("l.onError:" + t);
}
@Override
public void onDataAvailable() throws IOException
{
_history.add("l.onDataAvailable");
}
@Override
public void onAllDataRead() throws IOException
{
_history.add("l.onAllDataRead");
}
};
private HttpInput _in;
public class TContent extends HttpInput.Content
{
private final String _content;
public TContent(String content)
{
super(BufferUtil.toBuffer(content));
_content = content;
}
@Override
public void succeeded()
{
_history.add("Content succeeded " + _content);
super.succeeded();
}
@Override
public void failed(Throwable x)
{
_history.add("Content failed " + _content);
super.failed(x);
}
}
public class TestHttpInput extends HttpInput
{
public TestHttpInput(HttpChannelState state)
{
super(state);
}
@Override
protected void produceContent() throws IOException
{
_history.add("produceContent " + _fillAndParseSimulate.size());
for (String s = _fillAndParseSimulate.poll(); s != null; s = _fillAndParseSimulate.poll())
{
if ("_EOF_".equals(s))
_in.eof();
else
_in.addContent(new TContent(s));
}
}
@Override
protected void blockForContent() throws IOException
{
_history.add("blockForContent");
super.blockForContent();
}
}
public class TestHttpChannelState extends HttpChannelState
{
private boolean _fakeAsyncState;
public TestHttpChannelState(HttpChannel channel)
{
super(channel);
}
public boolean isFakeAsyncState()
{
return _fakeAsyncState;
}
public void setFakeAsyncState(boolean fakeAsyncState)
{
_fakeAsyncState = fakeAsyncState;
}
@Override
public boolean isAsyncStarted()
{
if (isFakeAsyncState())
return true;
return super.isAsyncStarted();
}
@Override
public void onReadUnready()
{
_history.add("s.onReadUnready");
super.onReadUnready();
}
@Override
public boolean onReadPossible()
{
_history.add("s.onReadPossible");
return super.onReadPossible();
}
@Override
public boolean onContentAdded()
{
_history.add("s.onDataAvailable");
return super.onContentAdded();
}
@Override
public boolean onReadReady()
{
_history.add("s.onReadReady");
return super.onReadReady();
}
}
@BeforeEach
public void before()
{
_in = new TestHttpInput(new TestHttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
{
@Override
public void onAsyncWaitForContent()
{
_history.add("asyncReadInterested");
}
})
);
}
@AfterEach
public void after()
{
assertThat(_history.poll(), nullValue());
}
@Test
public void testEmpty() throws Exception
{
assertThat(_in.available(), equalTo(0));
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.isReady(), equalTo(true));
assertThat(_history.poll(), nullValue());
}
@Test
public void testRead() throws Exception
{
_in.addContent(new TContent("AB"));
_in.addContent(new TContent("CD"));
_fillAndParseSimulate.offer("EF");
_fillAndParseSimulate.offer("GH");
assertThat(_in.available(), equalTo(2));
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.isReady(), equalTo(true));
assertThat(_in.getContentConsumed(), equalTo(0L));
assertThat(_in.read(), equalTo((int)'A'));
assertThat(_in.getContentConsumed(), equalTo(1L));
assertThat(_in.read(), equalTo((int)'B'));
assertThat(_in.getContentConsumed(), equalTo(2L));
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), nullValue());
assertThat(_in.read(), equalTo((int)'C'));
assertThat(_in.read(), equalTo((int)'D'));
assertThat(_history.poll(), equalTo("Content succeeded CD"));
assertThat(_history.poll(), nullValue());
assertThat(_in.read(), equalTo((int)'E'));
assertThat(_in.read(), equalTo((int)'F'));
assertThat(_history.poll(), equalTo("produceContent 2"));
assertThat(_history.poll(), equalTo("Content succeeded EF"));
assertThat(_history.poll(), nullValue());
assertThat(_in.read(), equalTo((int)'G'));
assertThat(_in.read(), equalTo((int)'H'));
assertThat(_history.poll(), equalTo("Content succeeded GH"));
assertThat(_history.poll(), nullValue());
assertThat(_in.getContentConsumed(), equalTo(8L));
assertThat(_history.poll(), nullValue());
}
@Test
public void testBlockingRead() throws Exception
{
new Thread(() ->
{
try
{
Thread.sleep(500);
_in.addContent(new TContent("AB"));
}
catch (Throwable th)
{
th.printStackTrace();
}
}).start();
assertThat(_in.read(), equalTo((int)'A'));
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("blockForContent"));
assertThat(_history.poll(), nullValue());
assertThat(_in.read(), equalTo((int)'B'));
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), nullValue());
}
@Test
public void testReadEOF() throws Exception
{
_in.addContent(new TContent("AB"));
_in.addContent(new TContent("CD"));
_in.eof();
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.available(), equalTo(2));
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.read(), equalTo((int)'A'));
assertThat(_in.read(), equalTo((int)'B'));
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), nullValue());
assertThat(_in.read(), equalTo((int)'C'));
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.read(), equalTo((int)'D'));
assertThat(_history.poll(), equalTo("Content succeeded CD"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.read(), equalTo(-1));
assertThat(_in.isFinished(), equalTo(true));
assertThat(_history.poll(), nullValue());
}
@Test
public void testReadEarlyEOF() throws Exception
{
_in.addContent(new TContent("AB"));
_in.addContent(new TContent("CD"));
_in.earlyEOF();
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.available(), equalTo(2));
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.read(), equalTo((int)'A'));
assertThat(_in.read(), equalTo((int)'B'));
assertThat(_in.read(), equalTo((int)'C'));
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.read(), equalTo((int)'D'));
assertThrows(EOFException.class, () -> _in.read());
assertTrue(_in.isFinished());
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), equalTo("Content succeeded CD"));
assertThat(_history.poll(), nullValue());
}
@Test
public void testBlockingEOF() throws Exception
{
new Thread(() ->
{
try
{
Thread.sleep(500);
_in.eof();
}
catch (Throwable th)
{
th.printStackTrace();
}
}).start();
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.read(), equalTo(-1));
assertThat(_in.isFinished(), equalTo(true));
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("blockForContent"));
assertThat(_history.poll(), nullValue());
}
@Test
public void testAsyncEmpty() throws Exception
{
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
_in.setReadListener(_listener);
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("s.onReadUnready"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(false));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(false));
assertThat(_history.poll(), nullValue());
}
@Test
public void testAsyncRead() throws Exception
{
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
_in.setReadListener(_listener);
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("s.onReadUnready"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(false));
assertThat(_history.poll(), nullValue());
_in.addContent(new TContent("AB"));
_fillAndParseSimulate.add("CD");
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
assertThat(_history.poll(), nullValue());
_in.run();
assertThat(_history.poll(), equalTo("l.onDataAvailable"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(true));
assertThat(_in.read(), equalTo((int)'A'));
assertThat(_in.isReady(), equalTo(true));
assertThat(_in.read(), equalTo((int)'B'));
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(true));
assertThat(_history.poll(), equalTo("produceContent 1"));
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
assertThat(_history.poll(), nullValue());
assertThat(_in.read(), equalTo((int)'C'));
assertThat(_in.isReady(), equalTo(true));
assertThat(_in.read(), equalTo((int)'D'));
assertThat(_history.poll(), equalTo("Content succeeded CD"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(false));
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("s.onReadUnready"));
assertThat(_history.poll(), nullValue());
}
@Test
public void testAsyncEOF() throws Exception
{
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
_in.setReadListener(_listener);
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("s.onReadUnready"));
assertThat(_history.poll(), nullValue());
_in.eof();
assertThat(_in.isReady(), equalTo(true));
assertThat(_in.isFinished(), equalTo(false));
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
assertThat(_history.poll(), nullValue());
assertThat(_in.read(), equalTo(-1));
assertThat(_in.isFinished(), equalTo(true));
assertThat(_history.poll(), nullValue());
}
@Test
public void testAsyncReadEOF() throws Exception
{
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
_in.setReadListener(_listener);
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("s.onReadUnready"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(false));
assertThat(_history.poll(), nullValue());
_in.addContent(new TContent("AB"));
_fillAndParseSimulate.add("_EOF_");
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
assertThat(_history.poll(), nullValue());
_in.run();
assertThat(_history.poll(), equalTo("l.onDataAvailable"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(true));
assertThat(_in.read(), equalTo((int)'A'));
assertThat(_in.isReady(), equalTo(true));
assertThat(_in.read(), equalTo((int)'B'));
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.isReady(), equalTo(true));
assertThat(_history.poll(), equalTo("produceContent 1"));
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isFinished(), equalTo(false));
assertThat(_in.read(), equalTo(-1));
assertThat(_in.isFinished(), equalTo(true));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(true));
assertThat(_history.poll(), nullValue());
}
@Test
public void testAsyncError() throws Exception
{
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
_in.setReadListener(_listener);
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), equalTo("s.onReadUnready"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(false));
assertThat(_history.poll(), nullValue());
_in.failed(new TimeoutException());
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
assertThat(_history.poll(), nullValue());
_in.run();
assertThat(_in.isFinished(), equalTo(true));
assertThat(_history.poll(), equalTo("l.onError:java.util.concurrent.TimeoutException"));
assertThat(_history.poll(), nullValue());
assertThat(_in.isReady(), equalTo(true));
IOException e = assertThrows(IOException.class, () -> _in.read());
assertThat(e.getCause(), instanceOf(TimeoutException.class));
assertThat(_in.isFinished(), equalTo(true));
assertThat(_history.poll(), nullValue());
}
@Test
public void testSetListenerWithNull() throws Exception
{
//test can't be null
assertThrows(NullPointerException.class, () ->
{
_in.setReadListener(null);
});
}
@Test
public void testSetListenerNotAsync() throws Exception
{
//test not async
assertThrows(IllegalStateException.class, () ->
{
_in.setReadListener(_listener);
});
}
@Test
public void testSetListenerAlreadySet() throws Exception
{
//set up a listener
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
_in.setReadListener(_listener);
//throw away any events generated by setting the listener
_history.clear();
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
//now test that you can't set another listener
assertThrows(IllegalStateException.class, () ->
{
_in.setReadListener(_listener);
});
}
@Test
public void testRecycle() throws Exception
{
testAsyncRead();
_in.recycle();
testAsyncRead();
_in.recycle();
testReadEOF();
}
@Test
public void testConsumeAll() throws Exception
{
_in.addContent(new TContent("AB"));
_in.addContent(new TContent("CD"));
_fillAndParseSimulate.offer("EF");
_fillAndParseSimulate.offer("GH");
assertThat(_in.read(), equalTo((int)'A'));
assertFalse(_in.consumeAll());
assertThat(_in.getContentConsumed(), equalTo(8L));
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), equalTo("Content succeeded CD"));
assertThat(_history.poll(), equalTo("produceContent 2"));
assertThat(_history.poll(), equalTo("Content succeeded EF"));
assertThat(_history.poll(), equalTo("Content succeeded GH"));
assertThat(_history.poll(), equalTo("produceContent 0"));
assertThat(_history.poll(), nullValue());
}
@Test
public void testConsumeAllEOF() throws Exception
{
_in.addContent(new TContent("AB"));
_in.addContent(new TContent("CD"));
_fillAndParseSimulate.offer("EF");
_fillAndParseSimulate.offer("GH");
_fillAndParseSimulate.offer("_EOF_");
assertThat(_in.read(), equalTo((int)'A'));
assertTrue(_in.consumeAll());
assertThat(_in.getContentConsumed(), equalTo(8L));
assertThat(_history.poll(), equalTo("Content succeeded AB"));
assertThat(_history.poll(), equalTo("Content succeeded CD"));
assertThat(_history.poll(), equalTo("produceContent 3"));
assertThat(_history.poll(), equalTo("Content succeeded EF"));
assertThat(_history.poll(), equalTo("Content succeeded GH"));
assertThat(_history.poll(), nullValue());
}
}

View File

@ -48,11 +48,41 @@ public class HttpWriterTest
HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
{ {
@Override
public boolean needContent()
{
return false;
}
@Override
public HttpInput.Content produceContent()
{
return null;
}
@Override
public boolean failAllContent(Throwable failure)
{
return false;
}
@Override @Override
public ByteBufferPool getByteBufferPool() public ByteBufferPool getByteBufferPool()
{ {
return pool; return pool;
} }
@Override
public boolean failed(Throwable x)
{
return false;
}
@Override
protected boolean eof()
{
return false;
}
}; };
_httpOut = new HttpOutput(channel) _httpOut = new HttpOutput(channel)

View File

@ -175,7 +175,38 @@ public class ResponseTest
{ {
_channelError = failure; _channelError = failure;
} }
}); })
{
@Override
public boolean needContent()
{
return false;
}
@Override
public HttpInput.Content produceContent()
{
return null;
}
@Override
public boolean failAllContent(Throwable failure)
{
return false;
}
@Override
public boolean failed(Throwable x)
{
return false;
}
@Override
protected boolean eof()
{
return false;
}
};
} }
@AfterEach @AfterEach

View File

@ -488,21 +488,8 @@ public class ServletHandler extends ScopedHandler
FilterChain chain = null; FilterChain chain = null;
// find the servlet // find the servlet
if (target.startsWith("/")) if (servletHolder != null && _filterMappings != null && _filterMappings.length > 0)
{ chain = getFilterChain(baseRequest, target.startsWith("/") ? target : null, servletHolder);
if (servletHolder != null && _filterMappings != null && _filterMappings.length > 0)
chain = getFilterChain(baseRequest, target, servletHolder);
}
else
{
if (servletHolder != null)
{
if (_filterMappings != null && _filterMappings.length > 0)
{
chain = getFilterChain(baseRequest, null, servletHolder);
}
}
}
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("chain={}", chain); LOG.debug("chain={}", chain);
@ -561,6 +548,7 @@ public class ServletHandler extends ScopedHandler
private FilterChain getFilterChain(Request baseRequest, String pathInContext, ServletHolder servletHolder) private FilterChain getFilterChain(Request baseRequest, String pathInContext, ServletHolder servletHolder)
{ {
Objects.requireNonNull(servletHolder);
String key = pathInContext == null ? servletHolder.getName() : pathInContext; String key = pathInContext == null ? servletHolder.getName() : pathInContext;
int dispatch = FilterMapping.dispatch(baseRequest.getDispatcherType()); int dispatch = FilterMapping.dispatch(baseRequest.getDispatcherType());
@ -576,7 +564,7 @@ public class ServletHandler extends ScopedHandler
// The mappings lists have been reversed to make this simple and fast. // The mappings lists have been reversed to make this simple and fast.
FilterChain chain = null; FilterChain chain = null;
if (servletHolder != null && _filterNameMappings != null && !_filterNameMappings.isEmpty()) if (_filterNameMappings != null && !_filterNameMappings.isEmpty())
{ {
if (_wildFilterNameMappings != null) if (_wildFilterNameMappings != null)
for (FilterMapping mapping : _wildFilterNameMappings) for (FilterMapping mapping : _wildFilterNameMappings)
@ -1658,6 +1646,7 @@ public class ServletHandler extends ScopedHandler
ChainEnd(ServletHolder holder) ChainEnd(ServletHolder holder)
{ {
Objects.requireNonNull(holder);
_servletHolder = holder; _servletHolder = holder;
} }

View File

@ -76,7 +76,7 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<tasks> <target>
<copy file="${project.build.directory}/deps.txt" tofile="${project.build.directory}/deps-orig.txt" /> <copy file="${project.build.directory}/deps.txt" tofile="${project.build.directory}/deps-orig.txt" />
<!-- regex the deps with classifiers first --> <!-- regex the deps with classifiers first -->
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/jnr/\2-\4-\3.jar" byline="true" /> <replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/jnr/\2-\4-\3.jar" byline="true" />
@ -88,7 +88,7 @@
<fileset file="${project.build.directory}/deps.txt" /> <fileset file="${project.build.directory}/deps.txt" />
<fileset file="src/main/config-template/modules/unixsocket-suffix.mod" /> <fileset file="src/main/config-template/modules/unixsocket-suffix.mod" />
</concat> </concat>
</tasks> </target>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -144,7 +144,7 @@ public class JarFileResource extends JarResource
String fileUrl = _urlString.substring(4, _urlString.length() - 2); String fileUrl = _urlString.substring(4, _urlString.length() - 2);
try try
{ {
return newResource(fileUrl).exists(); return _directory = newResource(fileUrl).exists();
} }
catch (Exception e) catch (Exception e)
{ {
@ -236,15 +236,10 @@ public class JarFileResource extends JarResource
return _exists; return _exists;
} }
/**
* Returns true if the represented resource is a container/directory.
* If the resource is not a file, resources ending with "/" are
* considered directories.
*/
@Override @Override
public boolean isDirectory() public boolean isDirectory()
{ {
return _urlString.endsWith("/") || exists() && _directory; return exists() && _directory;
} }
/** /**

View File

@ -320,8 +320,6 @@ public abstract class Resource implements ResourceFactory, Closeable
/** /**
* @return true if the represented resource is a container/directory. * @return true if the represented resource is a container/directory.
* if the resource is not a file, resources ending with "/" are
* considered directories.
*/ */
public abstract boolean isDirectory(); public abstract boolean isDirectory();
@ -412,7 +410,7 @@ public abstract class Resource implements ResourceFactory, Closeable
/** /**
* Returns the resource contained inside the current resource with the * Returns the resource contained inside the current resource with the
* given name. * given name, which may or may not exist.
* *
* @param path The path segment to add, which is not encoded * @param path The path segment to add, which is not encoded
* @return the Resource for the resolved path within this Resource, never null * @return the Resource for the resolved path within this Resource, never null

View File

@ -62,8 +62,19 @@ public class ResourceCollection extends Resource
* @param resources the resources to be added to collection * @param resources the resources to be added to collection
*/ */
public ResourceCollection(Resource... resources) public ResourceCollection(Resource... resources)
{
this(Arrays.asList(resources));
}
/**
* Instantiates a new resource collection.
*
* @param resources the resources to be added to collection
*/
public ResourceCollection(Collection<Resource> resources)
{ {
_resources = new ArrayList<>(); _resources = new ArrayList<>();
for (Resource r : resources) for (Resource r : resources)
{ {
if (r == null) if (r == null)
@ -82,17 +93,6 @@ public class ResourceCollection extends Resource
} }
} }
/**
* Instantiates a new resource collection.
*
* @param resources the resources to be added to collection
*/
public ResourceCollection(Collection<Resource> resources)
{
_resources = new ArrayList<>();
_resources.addAll(resources);
}
/** /**
* Instantiates a new resource collection. * Instantiates a new resource collection.
* *
@ -226,8 +226,16 @@ public class ResourceCollection extends Resource
} }
/** /**
* Add a path to the resource collection.
* @param path The path segment to add * @param path The path segment to add
* @return The contained resource (found first) in the collection of resources * @return The resulting resource(s) :
* <ul>
* <li>is a file that exists in at least one of the collection, then the first one found is returned</li>
* <li>is a directory that exists in at exactly one of the collection, then that directory resource is returned </li>
* <li>is a directory that exists in several of the collection, then a ResourceCollection of those directories is returned</li>
* <li>do not exist in any of the collection, then a new non existent resource relative to the first in the collection is returned.</li>
* </ul>
* @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed against any of the collection
*/ */
@Override @Override
public Resource addPath(String path) throws IOException public Resource addPath(String path) throws IOException
@ -247,27 +255,28 @@ public class ResourceCollection extends Resource
ArrayList<Resource> resources = null; ArrayList<Resource> resources = null;
// Attempt a simple (single) Resource lookup that exists // Attempt a simple (single) Resource lookup that exists
Resource addedResource = null;
for (Resource res : _resources) for (Resource res : _resources)
{ {
Resource r = res.addPath(path); addedResource = res.addPath(path);
if (!r.isDirectory() && r.exists()) if (!addedResource.exists())
{ continue;
// Return simple (non-directory) Resource if (!addedResource.isDirectory())
return r; return addedResource; // Return simple (non-directory) Resource
}
if (resources == null) if (resources == null)
{
resources = new ArrayList<>(); resources = new ArrayList<>();
} resources.add(addedResource);
}
resources.add(r); if (resources == null)
{
if (addedResource != null)
return addedResource; // This will not exist
return EmptyResource.INSTANCE;
} }
if (resources.size() == 1) if (resources.size() == 1)
{
return resources.get(0); return resources.get(0);
}
return new ResourceCollection(resources); return new ResourceCollection(resources);
} }
@ -384,7 +393,6 @@ public class ResourceCollection extends Resource
public boolean isDirectory() public boolean isDirectory()
{ {
assertResourcesSet(); assertResourcesSet();
return true; return true;
} }

View File

@ -128,11 +128,6 @@ public class URLResource extends Resource
return _in != null; return _in != null;
} }
/**
* Returns true if the represented resource is a container/directory.
* If the resource is not a file, resources ending with "/" are
* considered directories.
*/
@Override @Override
public boolean isDirectory() public boolean isDirectory()
{ {

View File

@ -35,7 +35,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -189,7 +188,7 @@ public class ResourceCollectionTest
assertThat(Arrays.asList(rc1.list()), contains("1.txt", "2.txt", "3.txt", "dir/")); assertThat(Arrays.asList(rc1.list()), contains("1.txt", "2.txt", "3.txt", "dir/"));
assertThat(Arrays.asList(rc1.addPath("dir").list()), contains("1.txt", "2.txt", "3.txt")); assertThat(Arrays.asList(rc1.addPath("dir").list()), contains("1.txt", "2.txt", "3.txt"));
assertThat(rc1.addPath("unknown").list(), emptyArray()); assertThat(rc1.addPath("unknown").list(), nullValue());
} }
@Test @Test

View File

@ -215,15 +215,15 @@ public class ResourceTest
cases.addCase(new Scenario(tdata1, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")); cases.addCase(new Scenario(tdata1, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
cases.addCase(new Scenario(tdata2, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")); cases.addCase(new Scenario(tdata2, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
cases.addCase(new Scenario("jar:file:/somejar.jar!/content/", !EXISTS, DIR)); cases.addCase(new Scenario("jar:file:/somejar.jar!/content/", !EXISTS, !DIR));
cases.addCase(new Scenario("jar:file:/somejar.jar!/", !EXISTS, DIR)); cases.addCase(new Scenario("jar:file:/somejar.jar!/", !EXISTS, !DIR));
String urlRef = cases.uriRef.toASCIIString(); String urlRef = cases.uriRef.toASCIIString();
Scenario zdata = new Scenario("jar:" + urlRef + "TestData/test.zip!/", EXISTS, DIR); Scenario zdata = new Scenario("jar:" + urlRef + "TestData/test.zip!/", EXISTS, DIR);
cases.addCase(zdata); cases.addCase(zdata);
cases.addCase(new Scenario(zdata, "Unknown", !EXISTS, !DIR)); cases.addCase(new Scenario(zdata, "Unknown", !EXISTS, !DIR));
cases.addCase(new Scenario(zdata, "/Unknown/", !EXISTS, DIR)); cases.addCase(new Scenario(zdata, "/Unknown/", !EXISTS, !DIR));
cases.addCase(new Scenario(zdata, "subdir", EXISTS, DIR)); cases.addCase(new Scenario(zdata, "subdir", EXISTS, DIR));
cases.addCase(new Scenario(zdata, "/subdir/", EXISTS, DIR)); cases.addCase(new Scenario(zdata, "/subdir/", EXISTS, DIR));

View File

@ -38,6 +38,7 @@ import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.security.ConstraintAware; import org.eclipse.jetty.security.ConstraintAware;
import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.authentication.FormAuthenticator; import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.FilterMapping; import org.eclipse.jetty.servlet.FilterMapping;
@ -745,7 +746,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor
case WebFragment: case WebFragment:
{ {
//a web-fragment set the value, all web-fragments must have the same value //a web-fragment set the value, all web-fragments must have the same value
if (!context.getSessionHandler().getSessionCookieConfig().getName().equals(name)) if (!name.equals(SessionHandler.getSessionCookieName(context.getSessionHandler().getSessionCookieConfig())))
throw new IllegalStateException("Conflicting cookie-config name " + name + " in " + descriptor.getResource()); throw new IllegalStateException("Conflicting cookie-config name " + name + " in " + descriptor.getResource());
break; break;
} }
@ -821,7 +822,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor
case WebFragment: case WebFragment:
{ {
//a web-fragment set the value, all web-fragments must have the same value //a web-fragment set the value, all web-fragments must have the same value
if (!context.getSessionHandler().getSessionCookieConfig().getPath().equals(path)) if (!path.equals(context.getSessionHandler().getSessionCookieConfig().getPath()))
throw new IllegalStateException("Conflicting cookie-config path " + path + " in " + descriptor.getResource()); throw new IllegalStateException("Conflicting cookie-config path " + path + " in " + descriptor.getResource());
break; break;
} }

View File

@ -31,7 +31,6 @@ import java.util.Collections;
import java.util.EventListener; import java.util.EventListener;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.core.extensions;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -28,10 +29,13 @@ import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.ExtensionConfig;
import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.core.TestFrameHandler; import org.eclipse.jetty.websocket.core.TestFrameHandler;
import org.eclipse.jetty.websocket.core.WebSocketServer; import org.eclipse.jetty.websocket.core.WebSocketServer;
import org.eclipse.jetty.websocket.core.client.CoreClientUpgradeRequest; import org.eclipse.jetty.websocket.core.client.CoreClientUpgradeRequest;
@ -291,7 +295,8 @@ public class PerMessageDeflaterBufferSizeTest
// We should now only be able to send this message in multiple frames as it exceeds deflate_buffer_size. // We should now only be able to send this message in multiple frames as it exceeds deflate_buffer_size.
String message = "0123456789"; String message = "0123456789";
clientHandler.sendText(message); ByteBuffer payload = toBuffer(message, true);
clientHandler.getCoreSession().sendFrame(new Frame(OpCode.TEXT, payload), Callback.NOOP, false);
// Verify the frame has been fragmented into multiple parts. // Verify the frame has been fragmented into multiple parts.
int numFrames = 0; int numFrames = 0;
@ -315,4 +320,18 @@ public class PerMessageDeflaterBufferSizeTest
assertNull(serverHandler.getError()); assertNull(serverHandler.getError());
assertNull(clientHandler.getError()); assertNull(clientHandler.getError());
} }
public ByteBuffer toBuffer(String string, boolean direct)
{
ByteBuffer buffer = BufferUtil.allocate(string.length(), direct);
BufferUtil.clearToFill(buffer);
BufferUtil.put(BufferUtil.toBuffer(string), buffer);
BufferUtil.flipToFlush(buffer, 0);
// Sanity checks.
assertThat(buffer.hasArray(), is(!direct));
assertThat(BufferUtil.toString(buffer), is(string));
return buffer;
}
} }

View File

@ -14,6 +14,10 @@
<filter> <filter>
<filter-name>wsuf-test</filter-name> <filter-name>wsuf-test</filter-name>
<filter-class>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-class> <filter-class>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-class>
<init-param>
<param-name>jetty.websocket.WebSocketMapping</param-name>
<param-value>jetty.websocket.defaultMapping</param-value>
</init-param>
</filter> </filter>
<filter-mapping> <filter-mapping>

View File

@ -91,6 +91,7 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
private final ServletContextHandler contextHandler; private final ServletContextHandler contextHandler;
private final WebSocketMapping webSocketMapping; private final WebSocketMapping webSocketMapping;
private final WebSocketComponents components;
private final FrameHandlerFactory frameHandlerFactory; private final FrameHandlerFactory frameHandlerFactory;
private final Executor executor; private final Executor executor;
private final Configuration.ConfigurationCustomizer customizer = new Configuration.ConfigurationCustomizer(); private final Configuration.ConfigurationCustomizer customizer = new Configuration.ConfigurationCustomizer();
@ -102,14 +103,15 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
* Main entry point for {@link JettyWebSocketServletContainerInitializer}. * Main entry point for {@link JettyWebSocketServletContainerInitializer}.
* *
* @param webSocketMapping the {@link WebSocketMapping} that this container belongs to * @param webSocketMapping the {@link WebSocketMapping} that this container belongs to
* @param webSocketComponents the {@link WebSocketComponents} instance to use * @param components the {@link WebSocketComponents} instance to use
* @param executor the {@link Executor} to use * @param executor the {@link Executor} to use
*/ */
JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents webSocketComponents, Executor executor) JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents components, Executor executor)
{ {
this.contextHandler = contextHandler; this.contextHandler = contextHandler;
this.webSocketMapping = webSocketMapping; this.webSocketMapping = webSocketMapping;
this.executor = executor; this.executor = executor;
this.components = components;
// Ensure there is a FrameHandlerFactory // Ensure there is a FrameHandlerFactory
JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class); JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class);
@ -155,6 +157,11 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
}); });
} }
public WebSocketComponents getWebSocketComponents()
{
return components;
}
@Override @Override
public Executor getExecutor() public Executor getExecutor()
{ {

View File

@ -92,11 +92,9 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain
private static JettyWebSocketServerContainer initialize(ServletContextHandler context) private static JettyWebSocketServerContainer initialize(ServletContextHandler context)
{ {
WebSocketComponents components = WebSocketServerComponents.ensureWebSocketComponents(context.getServer(), context.getServletContext()); WebSocketComponents components = WebSocketServerComponents.ensureWebSocketComponents(context.getServer(), context.getServletContext());
WebSocketMapping mapping = WebSocketMapping.ensureMapping(context.getServletContext(), WebSocketMapping.DEFAULT_KEY);
JettyWebSocketServerContainer container = JettyWebSocketServerContainer.ensureContainer(context.getServletContext()); JettyWebSocketServerContainer container = JettyWebSocketServerContainer.ensureContainer(context.getServletContext());
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("configureContext {} {} {}", container, mapping, components); LOG.debug("initialize {} {}", container, components);
return container; return container;
} }
@ -105,6 +103,8 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain
public void onStartup(Set<Class<?>> c, ServletContext context) public void onStartup(Set<Class<?>> c, ServletContext context)
{ {
ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context, "Jetty WebSocket SCI"); ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context, "Jetty WebSocket SCI");
JettyWebSocketServletContainerInitializer.initialize(contextHandler); JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.initialize(contextHandler);
if (LOG.isDebugEnabled())
LOG.debug("onStartup {}", container);
} }
} }

View File

@ -22,11 +22,9 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.DispatcherType;
import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
@ -39,7 +37,6 @@ import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -54,22 +51,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class ErrorCloseTest public class ErrorCloseTest
{ {
private Server server = new Server(); private final Server server = new Server();
private WebSocketClient client = new WebSocketClient(); private final WebSocketClient client = new WebSocketClient();
private ThrowingSocket serverSocket = new ThrowingSocket(); private final ThrowingSocket serverSocket = new ThrowingSocket();
private CountDownLatch serverCloseListener = new CountDownLatch(1); private final CountDownLatch serverCloseListener = new CountDownLatch(1);
private ServerConnector connector;
private URI serverUri; private URI serverUri;
@BeforeEach @BeforeEach
public void start() throws Exception public void start() throws Exception
{ {
connector = new ServerConnector(server); ServerConnector connector = new ServerConnector(server);
server.addConnector(connector); server.addConnector(connector);
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/"); contextHandler.setContextPath("/");
contextHandler.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
JettyWebSocketServletContainerInitializer.configure(contextHandler, (context, container) -> JettyWebSocketServletContainerInitializer.configure(contextHandler, (context, container) ->
{ {
container.addMapping("/", (req, resp) -> serverSocket); container.addMapping("/", (req, resp) -> serverSocket);
@ -140,7 +135,7 @@ public class ErrorCloseTest
serverSocket.methodsToThrow.add("onOpen"); serverSocket.methodsToThrow.add("onOpen");
EventSocket clientSocket = new EventSocket(); EventSocket clientSocket = new EventSocket();
try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketCoreSession.class)) try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class))
{ {
client.connect(clientSocket, serverUri).get(5, TimeUnit.SECONDS); client.connect(clientSocket, serverUri).get(5, TimeUnit.SECONDS);
assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS));

View File

@ -25,7 +25,6 @@ import java.net.ServerSocket;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -33,7 +32,6 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import javax.servlet.DispatcherType;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
@ -49,7 +47,6 @@ import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
import org.eclipse.jetty.websocket.tests.EchoSocket; import org.eclipse.jetty.websocket.tests.EchoSocket;
import org.eclipse.jetty.websocket.tests.GetAuthHeaderEndpoint; import org.eclipse.jetty.websocket.tests.GetAuthHeaderEndpoint;
import org.eclipse.jetty.websocket.tests.SimpleStatusServlet; import org.eclipse.jetty.websocket.tests.SimpleStatusServlet;
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -73,7 +70,7 @@ public class ClientConnectTest
{ {
private Server server; private Server server;
private WebSocketClient client; private WebSocketClient client;
private CountDownLatch serverLatch = new CountDownLatch(1); private final CountDownLatch serverLatch = new CountDownLatch(1);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <E extends Throwable> E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher<Throwable> errorMatcher) private <E extends Throwable> E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher<Throwable> errorMatcher)
@ -143,8 +140,6 @@ public class ClientConnectTest
}); });
}); });
context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
context.addServlet(new ServletHolder(new SimpleStatusServlet(404)), "/bogus"); context.addServlet(new ServletHolder(new SimpleStatusServlet(404)), "/bogus");
context.addServlet(new ServletHolder(new SimpleStatusServlet(200)), "/a-okay"); context.addServlet(new ServletHolder(new SimpleStatusServlet(200)), "/a-okay");
context.addServlet(new ServletHolder(new InvalidUpgradeServlet()), "/invalid-upgrade/*"); context.addServlet(new ServletHolder(new InvalidUpgradeServlet()), "/invalid-upgrade/*");

View File

@ -25,12 +25,10 @@ import java.nio.ByteBuffer;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.DispatcherType;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
@ -50,7 +48,6 @@ import org.eclipse.jetty.websocket.tests.ConnectMessageEndpoint;
import org.eclipse.jetty.websocket.tests.EchoSocket; import org.eclipse.jetty.websocket.tests.EchoSocket;
import org.eclipse.jetty.websocket.tests.ParamsEndpoint; import org.eclipse.jetty.websocket.tests.ParamsEndpoint;
import org.eclipse.jetty.websocket.tests.util.FutureWriteCallback; import org.eclipse.jetty.websocket.tests.util.FutureWriteCallback;
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -104,10 +101,7 @@ public class WebSocketClientTest
configuration.addMapping("/get-params", (req, resp) -> new ParamsEndpoint()); configuration.addMapping("/get-params", (req, resp) -> new ParamsEndpoint());
}); });
context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
server.setHandler(context); server.setHandler(context);
server.start(); server.start();
} }

View File

@ -40,7 +40,6 @@ import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.websocket.core.Configuration; import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -78,34 +77,40 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
private static final Logger LOG = LoggerFactory.getLogger(WebSocketUpgradeFilter.class); private static final Logger LOG = LoggerFactory.getLogger(WebSocketUpgradeFilter.class);
private static final AutoLock LOCK = new AutoLock(); private static final AutoLock LOCK = new AutoLock();
/**
* The init parameter name used to define {@link ServletContext} attribute used to share the {@link WebSocketMapping}.
*/
public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "jetty.websocket.WebSocketMapping";
/**
* Return any {@link WebSocketUpgradeFilter} already present on the {@link ServletContext}.
*
* @param servletContext the {@link ServletContext} to use.
* @return the configured default {@link WebSocketUpgradeFilter} instance.
*/
private static FilterHolder getFilter(ServletContext servletContext) private static FilterHolder getFilter(ServletContext servletContext)
{ {
ContextHandler contextHandler = Objects.requireNonNull(ContextHandler.getContextHandler(servletContext)); ContextHandler contextHandler = Objects.requireNonNull(ContextHandler.getContextHandler(servletContext));
ServletHandler servletHandler = contextHandler.getChildHandlerByClass(ServletHandler.class); ServletHandler servletHandler = contextHandler.getChildHandlerByClass(ServletHandler.class);
for (FilterHolder holder : servletHandler.getFilters()) for (FilterHolder holder : servletHandler.getFilters())
{ {
if (holder.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM) != null) if (holder.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM) != null)
return holder; return holder;
} }
return null; return null;
} }
/** /**
* Configure the default WebSocketUpgradeFilter. * Ensure a {@link WebSocketUpgradeFilter} is available on the provided {@link ServletContext},
* * a new filter will added if one does not already exist.
* <p>
* This will return the default {@link WebSocketUpgradeFilter} on the
* provided {@link ServletContext}, creating the filter if necessary.
* </p> * </p>
* <p> * <p>
* The default {@link WebSocketUpgradeFilter} is also available via * The default {@link WebSocketUpgradeFilter} is also available via
* the {@link ServletContext} attribute named {@code org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter} * the {@link ServletContext} attribute named {@code org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter}
* </p> * </p>
* *
* @param servletContext the {@link ServletContext} to use * @param servletContext the {@link ServletContext} to use.
* @return the configured default {@link WebSocketUpgradeFilter} instance * @return the configured default {@link WebSocketUpgradeFilter} instance.
*/ */
public static FilterHolder ensureFilter(ServletContext servletContext) public static FilterHolder ensureFilter(ServletContext servletContext)
{ {
@ -132,8 +137,6 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
} }
} }
public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping.key";
private final Configuration.ConfigurationCustomizer defaultCustomizer = new Configuration.ConfigurationCustomizer(); private final Configuration.ConfigurationCustomizer defaultCustomizer = new Configuration.ConfigurationCustomizer();
private WebSocketMapping mapping; private WebSocketMapping mapping;
@ -174,10 +177,9 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
final ServletContext context = config.getServletContext(); final ServletContext context = config.getServletContext();
String mappingKey = config.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM); String mappingKey = config.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM);
if (mappingKey != null) if (mappingKey == null)
mapping = WebSocketMapping.ensureMapping(context, mappingKey); throw new ServletException("the WebSocketMapping init param must be set");
else mapping = WebSocketMapping.ensureMapping(context, mappingKey);
mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(context));
String max = config.getInitParameter("idleTimeout"); String max = config.getInitParameter("idleTimeout");
if (max == null) if (max == null)

View File

@ -63,7 +63,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
public static WebSocketMapping getMapping(ServletContext servletContext, String mappingKey) public static WebSocketMapping getMapping(ServletContext servletContext, String mappingKey)
{ {
Object mappingObject = servletContext.getAttribute(mappingKey); Object mappingObject = servletContext.getAttribute(mappingKey);
if (mappingObject != null) if (mappingObject != null)
{ {
if (mappingObject instanceof WebSocketMapping) if (mappingObject instanceof WebSocketMapping)
@ -86,7 +85,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
public static WebSocketMapping ensureMapping(ServletContext servletContext, String mappingKey) public static WebSocketMapping ensureMapping(ServletContext servletContext, String mappingKey)
{ {
WebSocketMapping mapping = getMapping(servletContext, mappingKey); WebSocketMapping mapping = getMapping(servletContext, mappingKey);
if (mapping == null) if (mapping == null)
{ {
mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(servletContext)); mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(servletContext));
@ -135,7 +133,7 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
throw new IllegalArgumentException("Unrecognized path spec syntax [" + rawSpec + "]"); throw new IllegalArgumentException("Unrecognized path spec syntax [" + rawSpec + "]");
} }
public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping"; public static final String DEFAULT_KEY = "jetty.websocket.defaultMapping";
private final PathMappings<Negotiator> mappings = new PathMappings<>(); private final PathMappings<Negotiator> mappings = new PathMappings<>();
private final WebSocketComponents components; private final WebSocketComponents components;

View File

@ -28,6 +28,9 @@
<websocket.api.version>1.1.2</websocket.api.version> <websocket.api.version>1.1.2</websocket.api.version>
<jsp.version>9.0.29</jsp.version> <jsp.version>9.0.29</jsp.version>
<infinispan.version>9.4.8.Final</infinispan.version> <infinispan.version>9.4.8.Final</infinispan.version>
<infinispan.protostream.version>4.3.4.Final</infinispan.protostream.version>
<gson.version>2.8.6</gson.version>
<alpn.agent.version>2.0.10</alpn.agent.version>
<hazelcast.version>4.0.1</hazelcast.version> <hazelcast.version>4.0.1</hazelcast.version>
<conscrypt.version>2.5.1</conscrypt.version> <conscrypt.version>2.5.1</conscrypt.version>
<asm.version>9.0</asm.version> <asm.version>9.0</asm.version>
@ -448,7 +451,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId> <artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version> <version>3.0.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -36,6 +36,8 @@ import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPOutputStream;
import javax.servlet.AsyncContext; import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.ReadListener; import javax.servlet.ReadListener;
@ -71,8 +73,11 @@ import org.eclipse.jetty.server.HttpInput.Content;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandler.Context; import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.compression.CompressionPool;
import org.eclipse.jetty.util.compression.InflaterPool;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
@ -87,6 +92,7 @@ import static org.eclipse.jetty.http.client.Transport.HTTP;
import static org.eclipse.jetty.util.BufferUtil.toArray; import static org.eclipse.jetty.util.BufferUtil.toArray;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -776,10 +782,18 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
throw new IllegalStateException(); throw new IllegalStateException();
if (input.read() != 'X') if (input.read() != 'X')
throw new IllegalStateException(); throw new IllegalStateException();
if (!input.isReady()) if (input.isReady())
throw new IllegalStateException(); {
if (input.read() != -1) try
throw new IllegalStateException(); {
if (input.read() != -1)
throw new IllegalStateException();
}
catch (IOException e)
{
// ignore
}
}
} }
catch (IOException x) catch (IOException x)
{ {
@ -1204,8 +1218,8 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
{ {
case 0: case 0:
// null transform // null transform
if (content.isEmpty()) content.skip(content.remaining());
state++; state++;
return null; return null;
case 1: case 1:
@ -1254,7 +1268,7 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
} }
default: default:
return null; return content;
} }
} }
}); });
@ -1300,7 +1314,6 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
CountDownLatch clientLatch = new CountDownLatch(1); CountDownLatch clientLatch = new CountDownLatch(1);
String expected = String expected =
"S0" +
"S1" + "S1" +
"S2" + "S2" +
"S3S3" + "S3S3" +
@ -1345,6 +1358,316 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
assertTrue(clientLatch.await(10, TimeUnit.SECONDS)); assertTrue(clientLatch.await(10, TimeUnit.SECONDS));
} }
@ParameterizedTest
@ArgumentsSource(TransportProvider.class)
public void testAsyncEcho(Transport transport) throws Exception
{
init(transport);
scenario.start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
System.err.println("Service " + request);
AsyncContext asyncContext = request.startAsync();
ServletInputStream input = request.getInputStream();
input.setReadListener(new ReadListener()
{
@Override
public void onDataAvailable() throws IOException
{
while (input.isReady())
{
int b = input.read();
if (b >= 0)
{
// System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b));
response.getOutputStream().write(b);
}
else
return;
}
}
@Override
public void onAllDataRead() throws IOException
{
asyncContext.complete();
}
@Override
public void onError(Throwable x)
{
}
});
}
});
AsyncRequestContent contentProvider = new AsyncRequestContent();
CountDownLatch clientLatch = new CountDownLatch(1);
AtomicReference<Result> resultRef = new AtomicReference<>();
scenario.client.newRequest(scenario.newURI())
.method(HttpMethod.POST)
.path(scenario.servletPath)
.body(contentProvider)
.send(new BufferingResponseListener(16 * 1024 * 1024)
{
@Override
public void onComplete(Result result)
{
resultRef.set(result);
clientLatch.countDown();
}
});
for (int i = 0; i < 1_000_000; i++)
{
contentProvider.offer(BufferUtil.toBuffer("S" + i));
}
contentProvider.close();
assertTrue(clientLatch.await(30, TimeUnit.SECONDS));
assertThat(resultRef.get().isSucceeded(), Matchers.is(true));
assertThat(resultRef.get().getResponse().getStatus(), Matchers.equalTo(HttpStatus.OK_200));
}
@ParameterizedTest
@ArgumentsSource(TransportProvider.class)
public void testAsyncInterceptedTwice(Transport transport) throws Exception
{
init(transport);
scenario.start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
System.err.println("Service " + request);
final HttpInput httpInput = ((Request)request).getHttpInput();
httpInput.addInterceptor(new GzipHttpInputInterceptor(new InflaterPool(-1, true), ((Request)request).getHttpChannel().getByteBufferPool(), 1024));
httpInput.addInterceptor(content ->
{
ByteBuffer byteBuffer = content.getByteBuffer();
byte[] bytes = new byte[2];
bytes[1] = byteBuffer.get();
bytes[0] = byteBuffer.get();
return new Content(wrap(bytes));
});
AsyncContext asyncContext = request.startAsync();
ServletInputStream input = request.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
input.setReadListener(new ReadListener()
{
@Override
public void onDataAvailable() throws IOException
{
while (input.isReady())
{
int b = input.read();
if (b > 0)
{
// System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b));
out.write(b);
}
else if (b < 0)
return;
}
}
@Override
public void onAllDataRead() throws IOException
{
response.getOutputStream().write(out.toByteArray());
asyncContext.complete();
}
@Override
public void onError(Throwable x)
{
}
});
}
});
AsyncRequestContent contentProvider = new AsyncRequestContent();
CountDownLatch clientLatch = new CountDownLatch(1);
String expected =
"0S" +
"1S" +
"2S" +
"3S" +
"4S" +
"5S" +
"6S";
scenario.client.newRequest(scenario.newURI())
.method(HttpMethod.POST)
.path(scenario.servletPath)
.body(contentProvider)
.send(new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
if (result.isSucceeded())
{
Response response = result.getResponse();
assertThat(response.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
assertThat(getContentAsString(), Matchers.equalTo(expected));
clientLatch.countDown();
}
}
});
for (int i = 0; i < 7; i++)
{
contentProvider.offer(gzipToBuffer("S" + i));
contentProvider.flush();
}
contentProvider.close();
assertTrue(clientLatch.await(10, TimeUnit.SECONDS));
}
@ParameterizedTest
@ArgumentsSource(TransportProvider.class)
public void testAsyncInterceptedTwiceWithNulls(Transport transport) throws Exception
{
init(transport);
scenario.start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
System.err.println("Service " + request);
final HttpInput httpInput = ((Request)request).getHttpInput();
httpInput.addInterceptor(content ->
{
if (content.isEmpty())
return content;
// skip contents with odd numbers
ByteBuffer duplicate = content.getByteBuffer().duplicate();
duplicate.get();
byte integer = duplicate.get();
int idx = Character.getNumericValue(integer);
Content contentCopy = new Content(content.getByteBuffer().duplicate());
content.skip(content.remaining());
if (idx % 2 == 0)
return contentCopy;
return null;
});
httpInput.addInterceptor(content ->
{
if (content.isEmpty())
return content;
// reverse the bytes
ByteBuffer byteBuffer = content.getByteBuffer();
byte[] bytes = new byte[2];
bytes[1] = byteBuffer.get();
bytes[0] = byteBuffer.get();
return new Content(wrap(bytes));
});
AsyncContext asyncContext = request.startAsync();
ServletInputStream input = request.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
input.setReadListener(new ReadListener()
{
@Override
public void onDataAvailable() throws IOException
{
while (input.isReady())
{
int b = input.read();
if (b > 0)
{
// System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b));
out.write(b);
}
else if (b < 0)
return;
}
}
@Override
public void onAllDataRead() throws IOException
{
response.getOutputStream().write(out.toByteArray());
asyncContext.complete();
}
@Override
public void onError(Throwable x)
{
}
});
}
});
AsyncRequestContent contentProvider = new AsyncRequestContent();
CountDownLatch clientLatch = new CountDownLatch(1);
String expected =
"0S" +
"2S" +
"4S" +
"6S";
scenario.client.newRequest(scenario.newURI())
.method(HttpMethod.POST)
.path(scenario.servletPath)
.body(contentProvider)
.send(new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
if (result.isSucceeded())
{
Response response = result.getResponse();
assertThat(response.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
assertThat(getContentAsString(), Matchers.equalTo(expected));
clientLatch.countDown();
}
}
});
contentProvider.offer(BufferUtil.toBuffer("S0"));
contentProvider.flush();
contentProvider.offer(BufferUtil.toBuffer("S1"));
contentProvider.flush();
contentProvider.offer(BufferUtil.toBuffer("S2"));
contentProvider.flush();
contentProvider.offer(BufferUtil.toBuffer("S3"));
contentProvider.flush();
contentProvider.offer(BufferUtil.toBuffer("S4"));
contentProvider.flush();
contentProvider.offer(BufferUtil.toBuffer("S5"));
contentProvider.flush();
contentProvider.offer(BufferUtil.toBuffer("S6"));
contentProvider.close();
assertTrue(clientLatch.await(10, TimeUnit.SECONDS));
}
private ByteBuffer gzipToBuffer(String s) throws IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzos = new GZIPOutputStream(baos);
gzos.write(s.getBytes(StandardCharsets.ISO_8859_1));
gzos.close();
return BufferUtil.toBuffer(baos.toByteArray());
}
@ParameterizedTest @ParameterizedTest
@ArgumentsSource(TransportProvider.class) @ArgumentsSource(TransportProvider.class)
public void testWriteListenerFromOtherThread(Transport transport) throws Exception public void testWriteListenerFromOtherThread(Transport transport) throws Exception
@ -1387,18 +1710,21 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
})) }))
.send(); .send();
assertEquals(HttpStatus.OK_200, response.getStatus()); assertEquals(HttpStatus.OK_200, response.getStatus());
latch.countDown();
} }
catch (Throwable x) catch (Throwable x)
{ {
failures.offer(x); failures.offer(x);
} }
finally
{
latch.countDown();
}
} }
}); });
} }
assertTrue(latch.await(30, TimeUnit.SECONDS)); assertTrue(latch.await(30, TimeUnit.SECONDS));
assertTrue(failures.isEmpty()); assertThat(failures, empty());
} }
private static class Listener implements ReadListener, WriteListener private static class Listener implements ReadListener, WriteListener
@ -1527,10 +1853,11 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
} }
@Override @Override
public void stopServer() public void stopServer() throws Exception
{ {
checkScope(); checkScope();
scope.set(null); scope.set(null);
super.stopServer();
} }
} }
} }

View File

@ -138,7 +138,13 @@
<dependency> <dependency>
<groupId>org.infinispan.protostream</groupId> <groupId>org.infinispan.protostream</groupId>
<artifactId>protostream</artifactId> <artifactId>protostream</artifactId>
<version>4.2.2.Final</version> <version>${infinispan.protostream.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -92,7 +92,8 @@
<dependency> <dependency>
<groupId>org.mariadb.jdbc</groupId> <groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId> <artifactId>mariadb-java-client</artifactId>
<version>2.6.2</version> <version>2.7.0</version>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>