Skip to content

Commit a2ed41c

Browse files
Oblioolegz
authored andcommitted
Improve ServerlessMVC startup failure diagnostics and add coverage for non-serverless factory paths
Signed-off-by: Oblio <oblio.leitch@vermont.gov>
1 parent 7162d24 commit a2ed41c

3 files changed

Lines changed: 157 additions & 8 deletions

File tree

spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@
4040
* @author Oleg Zhurakousky
4141
* @since 4.x
4242
*/
43-
@AutoConfiguration(beforeName = "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration")
43+
@AutoConfiguration(beforeName = {
44+
"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration",
45+
"org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration",
46+
"org.springframework.boot.jetty.autoconfigure.servlet.JettyServletWebServerAutoConfiguration",
47+
"org.springframework.boot.undertow.autoconfigure.servlet.UndertowServletWebServerAutoConfiguration"
48+
})
4449
@Configuration(proxyBeanMethods = false)
4550
public class ServerlessAutoConfiguration {
4651
private static final Log LOGGER = LogFactory.getLog(ServerlessAutoConfiguration.class);

spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ public final class ServerlessMVC {
8080

8181
private volatile ServletWebServerApplicationContext applicationContext;
8282

83+
@Nullable
84+
private volatile Throwable startupFailure;
85+
8386
private final CountDownLatch contextStartupLatch = new CountDownLatch(1);
8487

8588
private final long initializationTimeout;
@@ -114,11 +117,13 @@ private void initializeContextAsync(Class<?>... componentClasses) {
114117
initContext(componentClasses);
115118
}
116119
catch (Exception e) {
117-
throw new IllegalStateException(e);
120+
this.startupFailure = e;
121+
LOGGER.error("Application failed to initialize.", e);
118122
}
119123
finally {
120124
contextStartupLatch.countDown();
121-
LOGGER.info("Application is started successfully.");
125+
LOGGER.info((this.startupFailure == null) ? "Application is started successfully."
126+
: "Application startup finished with errors.");
122127
}
123128
}).start();
124129
}
@@ -133,17 +138,17 @@ private void initContext(Class<?>... componentClasses) {
133138
}
134139

135140
public ConfigurableWebApplicationContext getApplicationContext() {
136-
this.waitForContext();
141+
this.assertContextReady();
137142
return this.applicationContext;
138143
}
139144

140145
public ServletContext getServletContext() {
141-
this.waitForContext();
146+
this.assertContextReady();
142147
return this.dispatcher.getServletContext();
143148
}
144149

145150
public void stop() {
146-
this.waitForContext();
151+
this.assertContextReady();
147152
this.applicationContext.stop();
148153
}
149154

@@ -154,8 +159,7 @@ public void stop() {
154159
* @param response the outgoing response
155160
*/
156161
public void service(HttpServletRequest request, HttpServletResponse response) throws Exception {
157-
Assert.state(this.waitForContext(), "Failed to initialize Application within the specified time of " + this.initializationTimeout + " milliseconds. "
158-
+ "If you need to increase it, please set " + INIT_TIMEOUT + " environment variable");
162+
this.assertContextReady();
159163
this.service(request, response, (CountDownLatch) null);
160164
}
161165

@@ -196,6 +200,18 @@ public boolean waitForContext() {
196200
return false;
197201
}
198202

203+
private void assertContextReady() {
204+
Assert.state(this.waitForContext(), "Failed to initialize Application within the specified time of " + this.initializationTimeout + " milliseconds. "
205+
+ "If you need to increase it, please set " + INIT_TIMEOUT + " environment variable");
206+
if (this.startupFailure != null) {
207+
throw new IllegalStateException("Application context failed to initialize. "
208+
+ "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.", this.startupFailure);
209+
}
210+
Assert.state(this.dispatcher != null, "DispatcherServlet is not initialized. "
211+
+ "Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.");
212+
Assert.state(this.applicationContext != null, "ApplicationContext is not initialized.");
213+
}
214+
199215
private static class ProxyFilterChain implements FilterChain {
200216

201217
@Nullable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2024-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.function.serverless.web;
18+
19+
import jakarta.servlet.http.HttpServletRequest;
20+
import org.junit.jupiter.api.AfterEach;
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.boot.autoconfigure.AutoConfiguration;
24+
import org.springframework.boot.autoconfigure.SpringBootApplication;
25+
import org.springframework.boot.web.server.WebServer;
26+
import org.springframework.boot.web.server.WebServerException;
27+
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
28+
import org.springframework.web.bind.annotation.GetMapping;
29+
import org.springframework.web.bind.annotation.RestController;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
33+
34+
class ServerlessAutoConfigurationTests {
35+
36+
@Test
37+
void autoConfigurationOrderingCoversSupportedServletContainers() {
38+
AutoConfiguration autoConfiguration = ServerlessAutoConfiguration.class.getAnnotation(AutoConfiguration.class);
39+
assertThat(autoConfiguration).isNotNull();
40+
41+
assertThat(autoConfiguration.beforeName()).contains(
42+
"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration",
43+
"org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration",
44+
"org.springframework.boot.jetty.autoconfigure.servlet.JettyServletWebServerAutoConfiguration",
45+
"org.springframework.boot.undertow.autoconfigure.servlet.UndertowServletWebServerAutoConfiguration");
46+
}
47+
48+
@Test
49+
void missingServerlessAutoConfigurationFailsWithUsefulError() {
50+
System.setProperty(ServerlessMVC.INIT_TIMEOUT, "5000");
51+
ServerlessMVC mvc = ServerlessMVC.INSTANCE(ApplicationWithoutServerlessAutoConfiguration.class);
52+
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/hello");
53+
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
54+
55+
assertThatThrownBy(() -> mvc.service(request, response))
56+
.isInstanceOf(IllegalStateException.class)
57+
.hasMessageContaining("Application context failed to initialize")
58+
.hasMessageContaining("Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.");
59+
}
60+
61+
@Test
62+
void customServletWebServerFactoryFailsWithUsefulErrorInsteadOfNpe() {
63+
System.setProperty(ServerlessMVC.INIT_TIMEOUT, "5000");
64+
ServerlessMVC mvc = ServerlessMVC.INSTANCE(ApplicationWithCustomServletWebServerFactory.class);
65+
HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/hello");
66+
ServerlessHttpServletResponse response = new ServerlessHttpServletResponse();
67+
68+
assertThatThrownBy(() -> mvc.service(request, response))
69+
.isInstanceOf(IllegalStateException.class)
70+
.hasMessageContaining("Application context failed to initialize")
71+
.hasMessageContaining("Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.");
72+
}
73+
74+
@Test
75+
void failedStartupGetServletContextThrowsUsefulErrorInsteadOfNpe() {
76+
System.setProperty(ServerlessMVC.INIT_TIMEOUT, "5000");
77+
ServerlessMVC mvc = ServerlessMVC.INSTANCE(ApplicationWithCustomServletWebServerFactory.class);
78+
79+
assertThatThrownBy(mvc::getServletContext)
80+
.isInstanceOf(IllegalStateException.class)
81+
.hasMessageContaining("Application context failed to initialize")
82+
.hasMessageContaining("Ensure ServerlessAutoConfiguration is active and selected as the ServletWebServerFactory.");
83+
}
84+
85+
@AfterEach
86+
void clearInitTimeoutOverride() {
87+
System.clearProperty(ServerlessMVC.INIT_TIMEOUT);
88+
}
89+
90+
@RestController
91+
@SpringBootApplication(excludeName = "org.springframework.cloud.function.serverless.web.ServerlessAutoConfiguration")
92+
static class ApplicationWithoutServerlessAutoConfiguration {
93+
94+
@GetMapping("/hello")
95+
String hello() {
96+
return "hello";
97+
}
98+
}
99+
100+
@RestController
101+
@SpringBootApplication
102+
static class ApplicationWithCustomServletWebServerFactory {
103+
104+
@GetMapping("/hello")
105+
String hello() {
106+
return "hello";
107+
}
108+
109+
@org.springframework.context.annotation.Bean
110+
ServletWebServerFactory customServletWebServerFactory() {
111+
return (initializers) -> new WebServer() {
112+
@Override
113+
public void start() throws WebServerException {
114+
}
115+
116+
@Override
117+
public void stop() throws WebServerException {
118+
}
119+
120+
@Override
121+
public int getPort() {
122+
return 0;
123+
}
124+
};
125+
}
126+
}
127+
128+
}

0 commit comments

Comments
 (0)