diff --git a/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc b/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc index d30a642f2..e0ce743bd 100644 --- a/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc @@ -291,7 +291,7 @@ For a Spring Boot Actuator application, some additional management endpoints are To enabled this endpoint you must set `management.endpoint.env.post.enabled=true` and set a regular expression to limit what properties may be set at `management.endpoint.env.post.valid-keys-regex`. * `/actuator/refresh` to re-load the bootstrap context and refresh the `@RefreshScope` beans. * `/actuator/restart` to close the `ApplicationContext` and restart it (disabled by default). -* `/actuator/pause` and `/actuator/resume` for calling the `Lifecycle` methods (`stop()` and `start()` on the `ApplicationContext`). +* `/actuator/pause` and `/actuator/resume` for calling `pause()` and `resume()` on every `PauseHandler` bean. A handler for Spring Integration `Pausable` components is provided. NOTE: While enabling the `POST` method for `/actuator/env` endpoint can provide flexibility and convenience in managing your application environment variables, it's critical to ensure that the endpoint is secured, monitored and valid keys to update are set to prevent potential security risks. diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/restart/RestartEndpoint.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/restart/RestartEndpoint.java index 661f3eb69..ac7b31901 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/restart/RestartEndpoint.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/restart/RestartEndpoint.java @@ -68,6 +68,8 @@ public class RestartEndpoint implements ApplicationListener pauseHandlers = Collections.emptyList(); + private volatile boolean paused; + private long timeout; // @ManagedAttribute @@ -93,12 +95,16 @@ public void onApplicationEvent(ContextRefreshedWithApplicationEvent input) { this.args = this.event.getArgs(); this.application = this.event.getSpringApplication(); this.application.addInitializers(new PostProcessorInitializer()); - this.pauseHandlers = this.context.getBeanProvider(PauseHandler.class) - .orderedStream() - .collect(Collectors.toList()); + collectPauseHandlers(); } } + private void collectPauseHandlers() { + this.pauseHandlers = this.context.getBeanProvider(PauseHandler.class) + .orderedStream() + .collect(Collectors.toList()); + } + @WriteOperation public Object restart() { Thread thread = new Thread(this::safeRestart); @@ -140,10 +146,14 @@ public synchronized ConfigurableApplicationContext doRestart() { } this.application.setEnvironment(this.context.getEnvironment()); close(); + // The handlers and the paused state belong to the context that was closed + this.pauseHandlers = Collections.emptyList(); + this.paused = false; // If running in a webapp then the context classloader is probably going to // die so we need to revert to a safe place before starting again overrideClassLoaderForRestart(); this.context = this.application.run(this.args); + collectPauseHandlers(); } return this.context; } @@ -174,6 +184,9 @@ public synchronized void doPause() { for (PauseHandler handler : this.pauseHandlers) { handler.pause(); } + if (!this.pauseHandlers.isEmpty()) { + this.paused = true; + } } // @ManagedOperation @@ -182,6 +195,7 @@ public synchronized void doResume() { PauseHandler handler = this.pauseHandlers.get(i); handler.resume(); } + this.paused = false; } private void overrideClassLoaderForRestart() { @@ -235,7 +249,7 @@ public class ResumeEndpoint { @WriteOperation public Boolean resume() { - if (!isRunning()) { + if (RestartEndpoint.this.paused) { doResume(); return true; } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/restart/RestartIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/restart/RestartIntegrationTests.java index c6ec85b47..bb590b849 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/restart/RestartIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/restart/RestartIntegrationTests.java @@ -22,6 +22,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.BDDAssertions.then; @@ -70,10 +71,82 @@ public void testRestartTwice() { */ } + @Test + public void testResumeAfterPause() { + this.context = SpringApplication.run(PauseHandlerConfiguration.class, + "--management.endpoint.restart.enabled=true", "--server.port=0", + "--management.endpoints.web.exposure.include=restart,pause,resume"); + + RestartEndpoint endpoint = this.context.getBean(RestartEndpoint.class); + TestPauseHandler handler = this.context.getBean(TestPauseHandler.class); + + then(endpoint.getResumeEndpoint().resume()).isFalse(); + then(endpoint.getPauseEndpoint().pause()).isTrue(); + then(handler.paused).isTrue(); + then(endpoint.getResumeEndpoint().resume()).isTrue(); + then(handler.paused).isFalse(); + then(endpoint.getResumeEndpoint().resume()).isFalse(); + } + + @Test + public void testRestartAfterPause() { + this.context = SpringApplication.run(PauseHandlerConfiguration.class, + "--management.endpoint.restart.enabled=true", "--server.port=0", + "--management.endpoints.web.exposure.include=restart,pause,resume"); + + RestartEndpoint endpoint = this.context.getBean(RestartEndpoint.class); + TestPauseHandler closed = this.context.getBean(TestPauseHandler.class); + + then(endpoint.getPauseEndpoint().pause()).isTrue(); + this.context = endpoint.doRestart(); + TestPauseHandler handler = this.context.getBean(TestPauseHandler.class); + then(handler).isNotSameAs(closed); + + then(endpoint.getResumeEndpoint().resume()).isFalse(); + then(endpoint.getPauseEndpoint().pause()).isTrue(); + then(handler.paused).isTrue(); + then(endpoint.getResumeEndpoint().resume()).isTrue(); + then(handler.paused).isFalse(); + } + + @Test + public void testResumeWithoutPauseHandlers() { + RestartEndpoint endpoint = new RestartEndpoint(); + endpoint.doPause(); + then(endpoint.getResumeEndpoint().resume()).isFalse(); + } + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration protected static class TestConfiguration { } + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + protected static class PauseHandlerConfiguration { + + @Bean + TestPauseHandler testPauseHandler() { + return new TestPauseHandler(); + } + + } + + static class TestPauseHandler implements PauseHandler { + + boolean paused; + + @Override + public void pause() { + this.paused = true; + } + + @Override + public void resume() { + this.paused = false; + } + + } + }