Skip to content

Commit 8f838d0

Browse files
authored
GH-9455: Introduce IntegrationKeepAlive (#9493)
Fixes: #9455 Issue link: #9455 * Add an `IntegrationKeepAlive` infrastructure bean to initiate a long-lived non-daemon thread to keep application alive when it cannot be kept like that for various reason, but has to. * Expose `spring.integration.keepAlive` global property to disable an `IntegrationKeepAlive` auto-startup * Test and document the feature
1 parent 52e8174 commit 8f838d0

File tree

12 files changed

+374
-1
lines changed

12 files changed

+374
-1
lines changed

Diff for: spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java

+13
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
4545
import org.springframework.integration.context.IntegrationContextUtils;
4646
import org.springframework.integration.context.IntegrationProperties;
47+
import org.springframework.integration.endpoint.management.IntegrationKeepAlive;
4748
import org.springframework.integration.handler.LoggingHandler;
4849
import org.springframework.integration.handler.support.IntegrationMessageHandlerMethodFactory;
4950
import org.springframework.integration.json.JsonPathUtils;
@@ -129,6 +130,7 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
129130
registerListMessageHandlerMethodFactory();
130131
registerIntegrationConfigurationReport();
131132
registerControlBusCommandRegistry();
133+
registerKeepAlive();
132134
}
133135

134136
@Override
@@ -460,4 +462,15 @@ private static BeanDefinitionBuilder createMessageHandlerMethodFactoryBeanDefini
460462
IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME);
461463
}
462464

465+
private void registerKeepAlive() {
466+
if (!this.beanFactory.containsBean(IntegrationContextUtils.INTEGRATION_KEEP_ALIVE_BEAN_NAME)) {
467+
BeanDefinitionBuilder builder =
468+
BeanDefinitionBuilder.genericBeanDefinition(IntegrationKeepAlive.class)
469+
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
470+
471+
this.registry.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_KEEP_ALIVE_BEAN_NAME,
472+
builder.getBeanDefinition());
473+
}
474+
}
475+
463476
}

Diff for: spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java

+10
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,18 @@ public abstract class IntegrationContextUtils {
100100

101101
public static final String LIST_MESSAGE_HANDLER_FACTORY_BEAN_NAME = "integrationListMessageHandlerMethodFactory";
102102

103+
/**
104+
* The bean name for the {@code org.springframework.integration.support.management.ControlBusCommandRegistry}.
105+
* @since 6.4
106+
*/
103107
public static final String CONTROL_BUS_COMMAND_REGISTRY_BEAN_NAME = "controlBusCommandRegistry";
104108

109+
/**
110+
* The bean name for the {@code org.springframework.integration.endpoint.management.IntegrationKeepAlive}.
111+
* @since 6.4
112+
*/
113+
public static final String INTEGRATION_KEEP_ALIVE_BEAN_NAME = "integrationKeepAlive";
114+
105115
/**
106116
* The default timeout for blocking operations like send and receive messages.
107117
* @since 6.1

Diff for: spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
/**
2727
* Utility class to encapsulate infrastructure Integration properties constants and their default values.
28-
* The default values can be overridden by the {@code META-INF/spring.integration.properties} with this entries
28+
* The default values can be overridden by the {@code META-INF/spring.integration.properties} with these entries
2929
* (includes their default values):
3030
* <ul>
3131
* <li> {@code spring.integration.channels.autoCreate=true}
@@ -38,6 +38,7 @@
3838
* <li> {@code spring.integration.channels.error.requireSubscribers=true}
3939
* <li> {@code spring.integration.channels.error.ignoreFailures=true}
4040
* <li> {@code spring.integration.endpoints.defaultTimeout=30000}
41+
* <li> {@code spring.integration.keepAlive=true}
4142
* </ul>
4243
*
4344
* @author Artem Bilan
@@ -117,6 +118,12 @@ public final class IntegrationProperties {
117118
*/
118119
public static final String ENDPOINTS_DEFAULT_TIMEOUT = INTEGRATION_PROPERTIES_PREFIX + "endpoints.defaultTimeout";
119120

121+
/**
122+
* Set to {@code false} to fully disable Keep-Alive thread.
123+
* @since 6.4
124+
*/
125+
public static final String KEEP_ALIVE = INTEGRATION_PROPERTIES_PREFIX + "keepAlive";
126+
120127
private static final Properties DEFAULTS;
121128

122129
private boolean channelsAutoCreate = true;
@@ -139,6 +146,8 @@ public final class IntegrationProperties {
139146

140147
private long endpointsDefaultTimeout = IntegrationContextUtils.DEFAULT_TIMEOUT;
141148

149+
private boolean keepAlive = true;
150+
142151
private volatile Properties properties;
143152

144153
static {
@@ -312,11 +321,30 @@ public long getEndpointsDefaultTimeout() {
312321
/**
313322
* Configure a value for {@link #ENDPOINTS_DEFAULT_TIMEOUT} option.
314323
* @param endpointsDefaultTimeout the value for {@link #ENDPOINTS_DEFAULT_TIMEOUT} option.
324+
* @since 6.2
315325
*/
316326
public void setEndpointsDefaultTimeout(long endpointsDefaultTimeout) {
317327
this.endpointsDefaultTimeout = endpointsDefaultTimeout;
318328
}
319329

330+
/**
331+
* Return the value of {@link #KEEP_ALIVE} option.
332+
* @return the value of {@link #KEEP_ALIVE} option.
333+
* @since 6.4
334+
*/
335+
public boolean isKeepAlive() {
336+
return this.keepAlive;
337+
}
338+
339+
/**
340+
* Configure a value for {@link #KEEP_ALIVE} option.
341+
* Defaults {@code true} - set to {@code false} disable keep-alive thread.
342+
* @param keepAlive {@code false} to disable keep-alive thread.
343+
*/
344+
public void setKeepAlive(boolean keepAlive) {
345+
this.keepAlive = keepAlive;
346+
}
347+
320348
/**
321349
* Represent the current instance as a {@link Properties}.
322350
* @return the {@link Properties} representation.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2024 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.integration.endpoint.management;
18+
19+
import java.time.Instant;
20+
import java.util.concurrent.CountDownLatch;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.atomic.AtomicBoolean;
23+
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
27+
import org.springframework.beans.BeansException;
28+
import org.springframework.beans.factory.BeanFactory;
29+
import org.springframework.beans.factory.BeanFactoryAware;
30+
import org.springframework.beans.factory.SmartInitializingSingleton;
31+
import org.springframework.context.SmartLifecycle;
32+
import org.springframework.integration.context.IntegrationContextUtils;
33+
import org.springframework.integration.context.IntegrationProperties;
34+
import org.springframework.integration.endpoint.AbstractPollingEndpoint;
35+
import org.springframework.scheduling.TaskScheduler;
36+
37+
/**
38+
* The component to keep an application alive when there are no non-daemon threads.
39+
* Some application might just not rely on the loops in specific threads for their logic.
40+
* Or target protocol to integrate with communicates via daemon threads.
41+
* <p>
42+
* A bean for this class is registered automatically by Spring Integration infrastructure.
43+
* It is started by application context for a blocked keep-alive dedicated thread
44+
* only if there is no {@link AbstractPollingEndpoint} beans in the application context
45+
* or {@link TaskScheduler} is configured for daemon (or virtual) threads.
46+
* <p>
47+
* Can be stopped (or started respectively) manually after injection into some target service if found redundant.
48+
* <p>
49+
* The {@link IntegrationProperties#KEEP_ALIVE} integration global
50+
* property can be set to {@code false} to disable this component regardless of the application logic.
51+
*
52+
* @author Artem Bilan
53+
*
54+
* @since 6.4
55+
*/
56+
public class IntegrationKeepAlive implements SmartLifecycle, SmartInitializingSingleton, BeanFactoryAware {
57+
58+
private static final Log LOG = LogFactory.getLog(IntegrationKeepAlive.class);
59+
60+
private final AtomicBoolean running = new AtomicBoolean();
61+
62+
private BeanFactory beanFactory;
63+
64+
private boolean autoStartup;
65+
66+
private volatile Thread keepAliveThread;
67+
68+
@Override
69+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
70+
this.beanFactory = beanFactory;
71+
}
72+
73+
@Override
74+
public void afterSingletonsInstantiated() {
75+
IntegrationProperties integrationProperties = IntegrationContextUtils.getIntegrationProperties(this.beanFactory);
76+
this.autoStartup =
77+
integrationProperties.isKeepAlive()
78+
&& (isTaskSchedulerDaemon() || !isAbstractPollingEndpointPresent());
79+
}
80+
81+
private boolean isTaskSchedulerDaemon() {
82+
TaskScheduler taskScheduler = IntegrationContextUtils.getTaskScheduler(this.beanFactory);
83+
AtomicBoolean isDaemon = new AtomicBoolean();
84+
CountDownLatch checkDaemonThreadLatch = new CountDownLatch(1);
85+
taskScheduler.schedule(() -> {
86+
isDaemon.set(Thread.currentThread().isDaemon());
87+
checkDaemonThreadLatch.countDown();
88+
}, Instant.now());
89+
90+
boolean logWarning = false;
91+
try {
92+
if (!checkDaemonThreadLatch.await(10, TimeUnit.SECONDS)) {
93+
logWarning = true;
94+
}
95+
}
96+
catch (InterruptedException ex) {
97+
logWarning = true;
98+
}
99+
if (logWarning) {
100+
LOG.warn("The 'IntegrationKeepAlive' cannot check a 'TaskScheduler' daemon threads status. " +
101+
"Falling back to 'keep-alive'");
102+
}
103+
return isDaemon.get();
104+
}
105+
106+
private boolean isAbstractPollingEndpointPresent() {
107+
return this.beanFactory.getBeanProvider(AbstractPollingEndpoint.class)
108+
.stream()
109+
.findAny()
110+
.isPresent();
111+
}
112+
113+
@Override
114+
public boolean isAutoStartup() {
115+
return this.autoStartup;
116+
}
117+
118+
@Override
119+
public void start() {
120+
if (this.running.compareAndSet(false, true)) {
121+
this.keepAliveThread =
122+
new Thread(() -> {
123+
while (true) {
124+
try {
125+
Thread.sleep(Long.MAX_VALUE);
126+
}
127+
catch (InterruptedException ex) {
128+
break;
129+
}
130+
}
131+
});
132+
this.keepAliveThread.setDaemon(false);
133+
this.keepAliveThread.setName("spring-integration-keep-alive");
134+
this.keepAliveThread.start();
135+
}
136+
}
137+
138+
@Override
139+
public void stop() {
140+
if (this.running.compareAndSet(true, false)) {
141+
this.keepAliveThread.interrupt();
142+
}
143+
}
144+
145+
@Override
146+
public boolean isRunning() {
147+
return this.running.get();
148+
}
149+
150+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/**
22
* Provides classes related to endpoint management.
33
*/
4+
@org.springframework.lang.NonNullApi
5+
@org.springframework.lang.NonNullFields
46
package org.springframework.integration.endpoint.management;

Diff for: spring-integration-core/src/main/resources/META-INF/spring.integration.default.properties

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ spring.integration.messagingTemplate.throwExceptionOnLateReply=false
99
spring.integration.readOnly.headers=
1010
spring.integration.endpoints.noAutoStartup=
1111
spring.integration.endpoints.defaultTimeout=30000
12+
spring.integration.keepAlive=true

Diff for: spring-integration-core/src/test/java/org/springframework/integration/context/IntegrationContextTests.java

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.beans.factory.annotation.Autowired;
2222
import org.springframework.beans.factory.annotation.Qualifier;
2323
import org.springframework.integration.endpoint.AbstractEndpoint;
24+
import org.springframework.integration.endpoint.management.IntegrationKeepAlive;
2425
import org.springframework.integration.test.util.TestUtils;
2526
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
2627
import org.springframework.test.annotation.DirtiesContext;
@@ -52,6 +53,9 @@ public class IntegrationContextTests {
5253
@Autowired
5354
private ThreadPoolTaskScheduler taskScheduler;
5455

56+
@Autowired
57+
private IntegrationKeepAlive integrationKeepAlive;
58+
5559
@Test
5660
public void testIntegrationContextComponents() {
5761
assertThat(this.integrationProperties.isMessagingTemplateThrowExceptionOnLateReply()).isTrue();
@@ -62,6 +66,7 @@ public void testIntegrationContextComponents() {
6266
assertThat(this.serviceActivator.isRunning()).isFalse();
6367
assertThat(this.serviceActivatorExplicit.isAutoStartup()).isTrue();
6468
assertThat(this.serviceActivatorExplicit.isRunning()).isTrue();
69+
assertThat(this.integrationKeepAlive.isRunning()).isTrue();
6570
}
6671

6772
}

0 commit comments

Comments
 (0)