Merge branch '6.1.x'

This commit is contained in:
Brian Clozel 2024-08-30 21:23:29 +02:00
commit 52c4ffa4d2
5 changed files with 145 additions and 5 deletions

View File

@ -44,6 +44,8 @@ import java.util.TimeZone;
import java.util.stream.Collectors;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConnection;
@ -920,7 +922,19 @@ public class MockHttpServletRequest implements HttpServletRequest {
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
Assert.state(this.asyncSupported, "Async not supported");
this.asyncStarted = true;
this.asyncContext = new MockAsyncContext(request, response);
MockAsyncContext newAsyncContext = new MockAsyncContext(request, response);
if (this.asyncContext != null) {
try {
AsyncEvent startEvent = new AsyncEvent(newAsyncContext);
for (AsyncListener asyncListener : this.asyncContext.getListeners()) {
asyncListener.onStartAsync(startEvent);
}
}
catch (IOException ex) {
// ignore failures
}
}
this.asyncContext = newAsyncContext;
return this.asyncContext;
}

View File

@ -30,6 +30,9 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.Test;
@ -663,6 +666,44 @@ class MockHttpServletRequestTests {
request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE));
}
@Test
void shouldRejectAsyncStartsIfUnsupported() {
assertThat(request.isAsyncStarted()).isFalse();
assertThatIllegalStateException().isThrownBy(request::startAsync);
}
@Test
void startAsyncShouldUpdateRequestState() {
assertThat(request.isAsyncStarted()).isFalse();
request.setAsyncSupported(true);
AsyncContext asyncContext = request.startAsync();
assertThat(request.isAsyncStarted()).isTrue();
}
@Test
void shouldNotifyAsyncListeners() {
request.setAsyncSupported(true);
AsyncContext asyncContext = request.startAsync();
TestAsyncListener testAsyncListener = new TestAsyncListener();
asyncContext.addListener(testAsyncListener);
asyncContext.complete();
assertThat(testAsyncListener.events).hasSize(1);
assertThat(testAsyncListener.events.get(0)).extracting("name").isEqualTo("onComplete");
}
@Test
void shouldNotifyAsyncListenersWhenNewAsyncStarted() {
request.setAsyncSupported(true);
AsyncContext asyncContext = request.startAsync();
TestAsyncListener testAsyncListener = new TestAsyncListener();
asyncContext.addListener(testAsyncListener);
AsyncContext newAsyncContext = request.startAsync();
assertThat(testAsyncListener.events).hasSize(1);
ListenerEvent listenerEvent = testAsyncListener.events.get(0);
assertThat(listenerEvent).extracting("name").isEqualTo("onStartAsync");
assertThat(listenerEvent.event.getAsyncContext()).isEqualTo(newAsyncContext);
}
private void assertEqualEnumerations(Enumeration<?> enum1, Enumeration<?> enum2) {
int count = 0;
while (enum1.hasMoreElements()) {
@ -672,4 +713,31 @@ class MockHttpServletRequestTests {
}
}
static class TestAsyncListener implements AsyncListener {
List<ListenerEvent> events = new ArrayList<>();
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
this.events.add(new ListenerEvent("onComplete", asyncEvent));
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
this.events.add(new ListenerEvent("onTimeout", asyncEvent));
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
this.events.add(new ListenerEvent("onError", asyncEvent));
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
this.events.add(new ListenerEvent("onStartAsync", asyncEvent));
}
}
record ListenerEvent(String name, AsyncEvent event) {}
}

View File

@ -119,13 +119,13 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
throw ex;
}
finally {
// If async is started, register a listener for completion notification.
if (request.isAsyncStarted()) {
// If async is started during the first dispatch, register a listener for completion notification.
if (request.isAsyncStarted() && request.getDispatcherType() == DispatcherType.REQUEST) {
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
}
// scope is opened for ASYNC dispatches, but the observation will be closed
// by the async listener.
else if (request.getDispatcherType() != DispatcherType.ASYNC){
else if (!isAsyncDispatch(request)) {
Throwable error = fetchException(request);
if (error != null) {
observation.error(error);
@ -180,6 +180,7 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
@Override
public void onStartAsync(AsyncEvent event) {
event.getAsyncContext().addListener(this);
}
@Override

View File

@ -22,11 +22,16 @@ import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -149,6 +154,21 @@ class ServerHttpObservationFilterTests {
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
}
@Test
void shouldRegisterListenerForAsyncStarts() throws Exception {
CustomAsyncFilter customAsyncFilter = new CustomAsyncFilter();
this.mockFilterChain = new MockFilterChain(new NoOpServlet(), customAsyncFilter);
this.request.setAsyncSupported(true);
this.request.setDispatcherType(DispatcherType.REQUEST);
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
customAsyncFilter.asyncContext.dispatch();
this.request.setDispatcherType(DispatcherType.ASYNC);
AsyncContext newAsyncContext = this.request.startAsync();
newAsyncContext.complete();
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
}
@Test
void shouldCloseObservationAfterAsyncError() throws Exception {
this.request.setAsyncSupported(true);
@ -210,6 +230,29 @@ class ServerHttpObservationFilterTests {
Assert.notNull(response, "response must not be null");
response.setHeader("X-Trace-Id", "badc0ff33");
}
}
@SuppressWarnings("serial")
static class NoOpServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
static class CustomAsyncFilter implements Filter {
AsyncContext asyncContext;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
this.asyncContext = servletRequest.startAsync();
filterChain.doFilter(servletRequest, servletResponse);
}
}
}

View File

@ -44,6 +44,8 @@ import java.util.TimeZone;
import java.util.stream.Collectors;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConnection;
@ -920,7 +922,19 @@ public class MockHttpServletRequest implements HttpServletRequest {
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
Assert.state(this.asyncSupported, "Async not supported");
this.asyncStarted = true;
this.asyncContext = new MockAsyncContext(request, response);
MockAsyncContext newAsyncContext = new MockAsyncContext(request, response);
if (this.asyncContext != null) {
try {
AsyncEvent startEvent = new AsyncEvent(newAsyncContext);
for (AsyncListener asyncListener : this.asyncContext.getListeners()) {
asyncListener.onStartAsync(startEvent);
}
}
catch (IOException ex) {
// ignore failures
}
}
this.asyncContext = newAsyncContext;
return this.asyncContext;
}