From 8d604350e432150637f1ea043c5cbaf97d8dd75b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 3 May 2023 16:01:41 +0200 Subject: [PATCH] Support for Project CRaC in DefaultLifecycleProcessor Includes stopForRestart/restartAfterStop functionality. Closes gh-30242 --- framework-platform/framework-platform.gradle | 1 + spring-context/spring-context.gradle | 1 + .../support/DefaultLifecycleProcessor.java | 96 ++++++++++++++++- .../DefaultLifecycleProcessorTests.java | 102 +++++++++++++----- 4 files changed, 172 insertions(+), 28 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 5f63a11fb03..df384a1d25c 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -110,6 +110,7 @@ dependencies { api("org.awaitility:awaitility:4.2.0") api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.codehaus.jettison:jettison:1.5.4") + api("org.crac:crac:0.1.3") api("org.dom4j:dom4j:2.1.4") api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.8") api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3") diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index 04d4e1e8c72..754bfcb72fe 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -22,6 +22,7 @@ dependencies { optional("org.aspectj:aspectjweaver") optional("org.apache.groovy:groovy") optional("org.apache-extras.beanshell:bsh") + optional("org.crac:crac") optional("org.hibernate:hibernate-validator") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 59bd2ca3e00..7510f11f514 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -43,16 +44,25 @@ import org.springframework.context.Phased; import org.springframework.context.SmartLifecycle; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Default implementation of the {@link LifecycleProcessor} strategy. * + *

Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in + * groups for specific phases, on startup/shutdown as well as for explicit start/stop + * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}. + * As of 6.1, this also includes support for JVM snapshot checkpoints (Project CRaC). + * * @author Mark Fisher * @author Juergen Hoeller * @since 3.0 */ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware { + private static final boolean cracPresent = + ClassUtils.isPresent("org.crac.Core", DefaultLifecycleProcessor.class.getClassLoader()); + private final Log logger = LogFactory.getLog(getClass()); private volatile long timeoutPerShutdownPhase = 30000; @@ -62,6 +72,16 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor @Nullable private volatile ConfigurableListableBeanFactory beanFactory; + @Nullable + private volatile Set stoppedBeans; + + + public DefaultLifecycleProcessor() { + if (cracPresent) { + new CracDelegate().registerResource(); + } + } + /** * Specify the maximum time allotted in milliseconds for the shutdown of @@ -100,6 +120,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor */ @Override public void start() { + this.stoppedBeans = null; startBeans(false); this.running = true; } @@ -120,6 +141,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor @Override public void onRefresh() { + this.stoppedBeans = null; startBeans(true); this.running = true; } @@ -138,12 +160,28 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor // Internal helpers + void stopForRestart() { + if (this.running) { + this.stoppedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>()); + stopBeans(); + this.running = false; + } + } + + void restartAfterStop() { + if (this.stoppedBeans != null) { + startBeans(true); + this.stoppedBeans = null; + this.running = true; + } + } + private void startBeans(boolean autoStartupOnly) { Map lifecycleBeans = getLifecycleBeans(); Map phases = new TreeMap<>(); lifecycleBeans.forEach((beanName, bean) -> { - if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())) { + if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { int phase = getPhase(bean); phases.computeIfAbsent( phase, @@ -156,6 +194,12 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } } + private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) { + Set stoppedBeans = this.stoppedBeans; + return (stoppedBeans != null ? stoppedBeans.contains(beanName) : + (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())); + } + /** * Start the specified bean as part of the given set of Lifecycle beans, * making sure that any beans that it depends on are started first. @@ -169,8 +213,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor for (String dependency : dependenciesForBean) { doStart(lifecycleBeans, dependency, autoStartupOnly); } - if (!bean.isRunning() && - (!autoStartupOnly || !(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())) { + if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) { if (logger.isTraceEnabled()) { logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); } @@ -187,6 +230,12 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } } + private boolean toBeStarted(String beanName, Lifecycle bean) { + Set stoppedBeans = this.stoppedBeans; + return (stoppedBeans != null ? stoppedBeans.contains(beanName) : + (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())); + } + private void stopBeans() { Map lifecycleBeans = getLifecycleBeans(); Map phases = new TreeMap<>(Comparator.reverseOrder()); @@ -219,6 +268,10 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } try { if (bean.isRunning()) { + Set stoppedBeans = this.stoppedBeans; + if (stoppedBeans != null) { + stoppedBeans.add(beanName); + } if (bean instanceof SmartLifecycle smartLifecycle) { if (logger.isTraceEnabled()) { logger.trace("Asking bean '" + beanName + "' of type [" + @@ -391,4 +444,41 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor */ private record LifecycleGroupMember(String name, Lifecycle bean) {} + + /** + * Inner class to avoid a hard dependency on Project CRaC at runtime. + * @since 6.1 + * @see org.crac.Core + */ + private class CracDelegate { + + public void registerResource() { + logger.debug("Registering JVM snapshot callback for Spring-managed lifecycle beans"); + org.crac.Core.getGlobalContext().register(new CracResourceAdapter()); + } + } + + + /** + * Resource adapter for Project CRaC, triggering a stop-and-restart cycle + * for Spring-managed lifecycle beans around a JVM snapshot checkpoint. + * @since 6.1 + * @see #stopForRestart() + * @see #restartAfterStop() + */ + private class CracResourceAdapter implements org.crac.Resource { + + @Override + public void beforeCheckpoint(org.crac.Context context) { + logger.debug("Stopping Spring-managed lifecycle beans before JVM snapshot checkpoint"); + stopForRestart(); + } + + @Override + public void afterRestore(org.crac.Context context) { + logger.debug("Restarting Spring-managed lifecycle beans after JVM snapshot restore"); + restartAfterStop(); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java index 9bcfa4e7a69..4cf9b08b9c1 100644 --- a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; /** * @author Mark Fisher + * @author Juergen Hoeller * @since 3.0 */ class DefaultLifecycleProcessorTests { @@ -58,12 +59,12 @@ class DefaultLifecycleProcessorTests { Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); assertThat(contextLifecycleProcessor).isNotNull(); assertThat(contextLifecycleProcessor).isSameAs(bean); - assertThat(new DirectFieldAccessor(contextLifecycleProcessor).getPropertyValue( - "timeoutPerShutdownPhase")).isEqualTo(1000L); + assertThat(new DirectFieldAccessor(contextLifecycleProcessor).getPropertyValue("timeoutPerShutdownPhase")) + .isEqualTo(1000L); } @Test - void singleSmartLifecycleAutoStartup() throws Exception { + void singleSmartLifecycleAutoStartup() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); @@ -79,7 +80,7 @@ class DefaultLifecycleProcessorTests { } @Test - void singleSmartLifecycleAutoStartupWithLazyInit() throws Exception { + void singleSmartLifecycleAutoStartupWithLazyInit() { StaticApplicationContext context = new StaticApplicationContext(); RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleBean.class); bd.setLazyInit(true); @@ -93,7 +94,7 @@ class DefaultLifecycleProcessorTests { } @Test - void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() throws Exception { + void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() { StaticApplicationContext context = new StaticApplicationContext(); RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleFactoryBean.class); bd.setLazyInit(true); @@ -107,7 +108,7 @@ class DefaultLifecycleProcessorTests { } @Test - void singleSmartLifecycleWithoutAutoStartup() throws Exception { + void singleSmartLifecycleWithoutAutoStartup() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(false); @@ -125,7 +126,7 @@ class DefaultLifecycleProcessorTests { } @Test - void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() throws Exception { + void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); @@ -148,7 +149,7 @@ class DefaultLifecycleProcessorTests { } @Test - void smartLifecycleGroupStartup() throws Exception { + void smartLifecycleGroupStartup() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forStartupTests(1, startedBeans); @@ -183,7 +184,7 @@ class DefaultLifecycleProcessorTests { } @Test - void contextRefreshThenStartWithMixedBeans() throws Exception { + void contextRefreshThenStartWithMixedBeans() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); @@ -218,7 +219,7 @@ class DefaultLifecycleProcessorTests { } @Test - void contextRefreshThenStopAndRestartWithMixedBeans() throws Exception { + void contextRefreshThenStopAndRestartWithMixedBeans() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); @@ -259,9 +260,62 @@ class DefaultLifecycleProcessorTests { context.close(); } + @Test + void contextRefreshThenStopForRestartWithMixedBeans() { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); + TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); + TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); + TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); + context.getBeanFactory().registerSingleton("smartBean1", smartBean1); + context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); + context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(smartBean1.isRunning()).isFalse(); + assertThat(smartBean2.isRunning()).isFalse(); + context.refresh(); + DefaultLifecycleProcessor lifecycleProcessor = (DefaultLifecycleProcessor) + new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + smartBean2.stop(); + simpleBean1.start(); + assertThat(startedBeans).hasSize(3); + assertThat(getPhase(startedBeans.get(0))).isEqualTo(-3); + assertThat(getPhase(startedBeans.get(1))).isEqualTo(5); + assertThat(getPhase(startedBeans.get(2))).isEqualTo(0); + lifecycleProcessor.stopForRestart(); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(smartBean1.isRunning()).isFalse(); + assertThat(smartBean2.isRunning()).isFalse(); + lifecycleProcessor.restartAfterStop(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isFalse(); + assertThat(simpleBean1.isRunning()).isTrue(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(startedBeans).hasSize(5); + assertThat(getPhase(startedBeans.get(3))).isEqualTo(0); + assertThat(getPhase(startedBeans.get(4))).isEqualTo(5); + context.start(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isTrue(); + assertThat(simpleBean2.isRunning()).isTrue(); + assertThat(startedBeans).hasSize(7); + assertThat(getPhase(startedBeans.get(5))).isEqualTo(-3); + assertThat(getPhase(startedBeans.get(6))).isEqualTo(0); + context.close(); + } + @Test @EnabledForTestGroups(LONG_RUNNING) - void smartLifecycleGroupShutdown() throws Exception { + void smartLifecycleGroupShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans); @@ -292,7 +346,7 @@ class DefaultLifecycleProcessorTests { @Test @EnabledForTestGroups(LONG_RUNNING) - void singleSmartLifecycleShutdown() throws Exception { + void singleSmartLifecycleShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans); StaticApplicationContext context = new StaticApplicationContext(); @@ -307,7 +361,7 @@ class DefaultLifecycleProcessorTests { } @Test - void singleLifecycleShutdown() throws Exception { + void singleLifecycleShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean = new TestLifecycleBean(null, stoppedBeans); StaticApplicationContext context = new StaticApplicationContext(); @@ -324,7 +378,7 @@ class DefaultLifecycleProcessorTests { } @Test - void mixedShutdown() throws Exception { + void mixedShutdown() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans); Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans); @@ -373,7 +427,7 @@ class DefaultLifecycleProcessorTests { } @Test - void dependencyStartedFirstEvenIfItsPhaseIsHigher() throws Exception { + void dependencyStartedFirstEvenIfItsPhaseIsHigher() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); @@ -403,7 +457,7 @@ class DefaultLifecycleProcessorTests { @Test @EnabledForTestGroups(LONG_RUNNING) - void dependentShutdownFirstEvenIfItsPhaseIsLower() throws Exception { + void dependentShutdownFirstEvenIfItsPhaseIsLower() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); @@ -446,7 +500,7 @@ class DefaultLifecycleProcessorTests { } @Test - void dependencyStartedFirstAndIsSmartLifecycle() throws Exception { + void dependencyStartedFirstAndIsSmartLifecycle() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); @@ -478,7 +532,7 @@ class DefaultLifecycleProcessorTests { @Test @EnabledForTestGroups(LONG_RUNNING) - void dependentShutdownFirstAndIsSmartLifecycle() throws Exception { + void dependentShutdownFirstAndIsSmartLifecycle() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans); @@ -520,7 +574,7 @@ class DefaultLifecycleProcessorTests { } @Test - void dependencyStartedFirstButNotSmartLifecycle() throws Exception { + void dependencyStartedFirstButNotSmartLifecycle() { CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); @@ -544,7 +598,7 @@ class DefaultLifecycleProcessorTests { @Test @EnabledForTestGroups(LONG_RUNNING) - void dependentShutdownFirstButNotSmartLifecycle() throws Exception { + void dependentShutdownFirstButNotSmartLifecycle() { CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); @@ -583,8 +637,7 @@ class DefaultLifecycleProcessorTests { private static int getPhase(Lifecycle lifecycle) { - return (lifecycle instanceof SmartLifecycle) ? - ((SmartLifecycle) lifecycle).getPhase() : 0; + return (lifecycle instanceof SmartLifecycle ? ((SmartLifecycle) lifecycle).getPhase() : 0); } @@ -596,7 +649,6 @@ class DefaultLifecycleProcessorTests { private volatile boolean running; - static TestLifecycleBean forStartupTests(CopyOnWriteArrayList startedBeans) { return new TestLifecycleBean(startedBeans, null); } @@ -734,7 +786,7 @@ class DefaultLifecycleProcessorTests { DummySmartLifecycleBean bean = new DummySmartLifecycleBean(); @Override - public Object getObject() throws Exception { + public Object getObject() { return this.bean; }