Skip to content

Commit bc93313

Browse files
Feature/32 filter request timeout class (#90)
* Create RequestTimeOutFilter and RequestTimeOutFilterTest Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Revert "Create RequestTimeOutFilter and RequestTimeOutFilterTest" This reverts commit efc883d. * Create RequestTimeOutFilter and RequestTimeOutFilterTest Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Add test for exception in RequestTimeOutFilterTest Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Add a test for exception in RequestTimeOutFilterTest Changes by ebbaandersson * Add javadocs for RequestTimeOutFilter Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Changes made after CodeRabbit Review Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Change ExecutorService, add rejectedExecutionException, add method handleInternalError Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Changes tests to assert the new code in RequestTimeOutFilter * Create RequestTimeOutFilter and RequestTimeOutFilterTest Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Add test for exception in RequestTimeOutFilterTest Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Add a test for exception in RequestTimeOutFilterTest Changes by ebbaandersson * Add javadocs for RequestTimeOutFilter Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Changes made after CodeRabbit Review Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Change ExecutorService, add rejectedExecutionException, add method handleInternalError Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Changes tests to assert the new code in RequestTimeOutFilter * add a future.cancel to stop the worker task. add defensive copying and unmodifiable views of setter and getter in HttpResponseBuilder Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Removes "static" from ExecutorService. Adds a test which checks that the timeout-time is bigger than zero Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> * Adds chain-execution verification to happy-path-test. * Implementing a transfer from existing respons to shadowResponse in the beginning to make sure we don't lose information Co-authored-by: Ebba Andersson <eeebbaandersson@gmail.com> --------- Co-authored-by: Fredrik Mohlen <fredrikmohlen@gmail.com>
1 parent 245e188 commit bc93313

3 files changed

Lines changed: 231 additions & 1 deletion

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package org.example.filter;
2+
3+
import org.example.http.HttpResponseBuilder;
4+
import org.example.httpparser.HttpRequest;
5+
import static org.example.http.HttpResponseBuilder.*;
6+
7+
import java.util.Map;
8+
import java.util.concurrent.*;
9+
import java.util.logging.Logger;
10+
11+
/**
12+
* A proactive filter that monitors the execution time of the request processing chain.
13+
* If the execution exceeds the specified timeout, the filter interrupts the
14+
* processing thread and returns an HTTP 504 Gateway Timeout response.
15+
*/
16+
public class RequestTimeOutFilter implements Filter {
17+
18+
private final int timeoutMS;
19+
private static final Logger logger = Logger.getLogger(RequestTimeOutFilter.class.getName());
20+
21+
/** Thread pool used to execute the filter chain asynchronously for timeout monitoring. */
22+
private final ExecutorService executor = new ThreadPoolExecutor(
23+
Math.max(4, Runtime.getRuntime().availableProcessors() * 2),
24+
Math.max(4, Runtime.getRuntime().availableProcessors() * 2),
25+
60L, TimeUnit.SECONDS,
26+
new ArrayBlockingQueue<>(50),
27+
new ThreadPoolExecutor.AbortPolicy()
28+
);
29+
30+
public RequestTimeOutFilter(int timeoutMS) {
31+
32+
if (timeoutMS <= 0) {
33+
throw new IllegalArgumentException("timeoutMS must be greater than 0");
34+
}
35+
this.timeoutMS = timeoutMS;
36+
}
37+
38+
@Override
39+
public void init() {}
40+
41+
@Override
42+
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {
43+
44+
HttpResponseBuilder shadowResponse = new HttpResponseBuilder();
45+
// Preserve state already present on the real response before downstream execution
46+
transferResponseData(response, shadowResponse);
47+
48+
Future<?> future;
49+
try {
50+
future = executor.submit(() -> {
51+
try {
52+
chain.doFilter(request, shadowResponse);
53+
} catch (Exception e) {
54+
throw new RuntimeException(e);
55+
}
56+
});
57+
58+
} catch (RejectedExecutionException e) {
59+
logger.severe("SERVER OVERLOADED: Queue is full for path " + request.getPath());
60+
response.setStatusCode(SC_SERVICE_UNAVAILABLE);
61+
response.setHeaders(Map.of("Content-Type", "text/html; charset=utf-8"));
62+
response.setBody("<h1>503 Service Unavailable</h1><p>Server is too busy to handle the request.</p>");
63+
return;
64+
}
65+
66+
try {
67+
future.get(timeoutMS, TimeUnit.MILLISECONDS);
68+
transferResponseData(shadowResponse, response);
69+
70+
} catch (TimeoutException e) {
71+
future.cancel(true);
72+
logger.warning("TIMEOUT ERROR: " + request.getPath() + " was interrupted after " + timeoutMS + "ms");
73+
74+
response.setStatusCode(SC_GATEWAY_TIMEOUT);
75+
response.setHeaders(Map.of("Content-Type", "text/html; charset=utf-8"));
76+
response.setBody("<h1>504 Gateway Timeout</h1><p>The server took too long to respond.</p>");
77+
78+
return;
79+
80+
} catch (InterruptedException e) {
81+
Thread.currentThread().interrupt();
82+
future.cancel(true);
83+
handleInternalError(response, e);
84+
return;
85+
86+
} catch (ExecutionException e) {
87+
handleInternalError(response, e);
88+
return;
89+
}
90+
}
91+
private void transferResponseData(HttpResponseBuilder source, HttpResponseBuilder target) {
92+
target.setStatusCode(source.getStatusCode());
93+
target.setHeaders(source.getHeaders());
94+
95+
byte[] sourceBytes = source.getByteBody();
96+
if (sourceBytes != null) {
97+
target.setBody(sourceBytes);
98+
} else {
99+
target.setBody(source.getBody());
100+
}
101+
}
102+
103+
private void handleInternalError(HttpResponseBuilder response, Exception e) {
104+
logger.severe("Error during execution: " + e.getMessage());
105+
response.setStatusCode(SC_INTERNAL_SERVER_ERROR);
106+
response.setHeaders(Map.of("Content-Type", "text/html; charset=utf-8"));
107+
response.setBody("<h1>500 Internal Server Error</h1>");
108+
}
109+
110+
@Override
111+
public void destroy() {
112+
executor.shutdown();
113+
try {
114+
if(!executor.awaitTermination(5, TimeUnit.SECONDS)) {
115+
executor.shutdownNow();
116+
}
117+
} catch (InterruptedException e) {
118+
executor.shutdownNow();
119+
Thread.currentThread().interrupt();
120+
}
121+
}
122+
}

src/main/java/org/example/http/HttpResponseBuilder.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.example.http;
22

33
import java.nio.charset.StandardCharsets;
4+
import java.util.Arrays;
5+
import java.util.Collections;
46
import java.util.Map;
57
import java.util.TreeMap;
68

@@ -75,7 +77,7 @@ public void setBody(String body) {
7577
}
7678

7779
public void setBody(byte[] body) {
78-
this.bytebody = body;
80+
this.bytebody = body == null ? null : Arrays.copyOf(body, body.length);
7981
this.body = "";
8082
}
8183

@@ -146,4 +148,19 @@ public byte[] build() {
146148

147149
return response;
148150
}
151+
152+
public Map<String, String> getHeaders() {
153+
TreeMap<String, String> copy = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
154+
copy.putAll(headers);
155+
return Collections.unmodifiableMap(copy);
156+
}
157+
158+
public String getBody(){
159+
return body;
160+
}
161+
162+
public byte[] getByteBody() {
163+
164+
return bytebody == null ? null : Arrays.copyOf(bytebody, bytebody.length);
165+
}
149166
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package org.example.filter;
2+
3+
import org.example.http.HttpResponseBuilder;
4+
import org.example.httpparser.HttpRequest;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.util.concurrent.atomic.AtomicBoolean;
9+
10+
import static org.example.http.HttpResponseBuilder.*;
11+
12+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
13+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
14+
15+
class RequestTimeOutFilterTest {
16+
17+
private RequestTimeOutFilter filter;
18+
private HttpResponseBuilder response;
19+
private HttpRequest request;
20+
21+
@BeforeEach
22+
void setUp() {
23+
filter = new RequestTimeOutFilter(100);
24+
response = new HttpResponseBuilder();
25+
request = new HttpRequest("GET", "/", "HTTP/1.1",null,"");
26+
}
27+
28+
29+
// Happy Path --> Allt går bra
30+
@Test
31+
void requestTimeOutFilter_shouldSucceedWhenFast() {
32+
33+
// Arrange --> FilterChain som körs utan fördröjning
34+
AtomicBoolean chainInvoked = new AtomicBoolean(false);
35+
FilterChain fastChain = (request, response) -> chainInvoked.set(true);
36+
37+
// Act
38+
filter.doFilter(request, response, fastChain);
39+
40+
// Assert
41+
assertThat(response.getStatusCode()).isEqualTo(SC_OK);
42+
assertThat(chainInvoked.get()).isTrue();
43+
}
44+
45+
// Timeout Path --> Anropet tar för lång tid och kastar RunTimeException
46+
@Test
47+
void requestTimeOutFilter_shouldReturn504ResponseWhenSlow() {
48+
// Arrange --> En simulation av en fördröjning
49+
FilterChain slowChain = (request, response) -> {
50+
try {
51+
Thread.sleep(600);
52+
} catch (InterruptedException e) {
53+
Thread.currentThread().interrupt();
54+
}
55+
};
56+
57+
// Act
58+
filter.doFilter(request, response, slowChain);
59+
60+
// Assert
61+
assertThat(response.getStatusCode())
62+
.as("Status code should be 504 at timeout")
63+
.isEqualTo(SC_GATEWAY_TIMEOUT);
64+
65+
assertThat(new String(response.build()))
66+
.contains("Gateway Timeout");
67+
}
68+
69+
// Exception Path --> Oväntat undantag kastar en exception
70+
@Test
71+
void requestTimeOutFilter_shouldHandleGenericException() {
72+
// Arrange
73+
FilterChain errorChain = (request, response) -> {
74+
throw new RuntimeException("Unexpected error");
75+
};
76+
77+
// Act
78+
filter.doFilter(request, response, errorChain);
79+
80+
// Assert
81+
assertThat(response.getStatusCode()).isEqualTo((SC_INTERNAL_SERVER_ERROR));
82+
83+
}
84+
@Test
85+
void constructor_shouldRejectNonPositiveTimeout() {
86+
assertThatThrownBy(() -> new RequestTimeOutFilter(0))
87+
.isInstanceOf(IllegalArgumentException.class);
88+
assertThatThrownBy(() -> new RequestTimeOutFilter(-1))
89+
.isInstanceOf(IllegalArgumentException.class);
90+
}
91+
}

0 commit comments

Comments
 (0)