Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh Scope on restart. #1266

Merged
merged 6 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ The configuration property must be present in order to update the value after a
a value in your application you might want to switch your logic to rely on its absence instead. Another option would be to rely
on the value changing rather than not being present in the application's configuration.

[refresh-scope-on-restart]
=== Refresh Scope on Restart

In order to allow seamlessly refreshing beans on restart, which is especially useful for applications running with JVM Checkpoint Restore (for example with https://github.com/CRaC[Project CRaC]), we now instantiate a `RefreshScopeLifecycle` bean that will trigger Context Refresh on restart, resulting in rebinding configuration properties and refreshing any `@RefreshScope`-annotated beans. This behaviour can be disabled by setting the value of `spring.cloud.refresh.on-restart.enabled` to `false`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That long and complex sentence is hard to parse in one pass, so please change it to:

Seamlessly refreshing beans on restart is especially useful for applications that run with JVM Checkpoint Restore (such as https://github.com/CRaC[Project CRaC]). To allow this ability, we now instantiate a RefreshScopeLifecycle bean that triggers Context Refresh on restart, resulting in rebinding configuration properties and refreshing any beans annotated with @RefreshScope. You can disable this behavior by setting spring.cloud.refresh.on-restart.enabled to false.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Buzzardo - will change.



[[encryption-and-decryption]]
== Encryption and Decryption

Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/partials/_configprops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
|spring.cloud.refresh.enabled | `+++true+++` | Enables autoconfiguration for the refresh scope and associated features.
|spring.cloud.refresh.extra-refreshable | `+++true+++` | Additional class names for beans to post process into refresh scope.
|spring.cloud.refresh.never-refreshable | `+++true+++` | Comma separated list of class names for beans to never be refreshed or rebound.
|spring.cloud.refresh.on-restart.enabled | `+++true+++` | Enable refreshing context on start.
|spring.cloud.service-registry.auto-registration.enabled | `+++true+++` | Whether service auto-registration is enabled. Defaults to true.
|spring.cloud.service-registry.auto-registration.fail-fast | `+++false+++` | Whether startup fails if there is no AutoServiceRegistration. Defaults to false.
|spring.cloud.service-registry.auto-registration.register-management | `+++true+++` | Whether to register the management as a service. Defaults to true.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.springframework.cloud.context.refresh.ConfigDataContextRefresher;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.cloud.context.refresh.LegacyContextRefresher;
import org.springframework.cloud.context.refresh.RefreshScopeLifecycle;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.cloud.endpoint.event.RefreshEventListener;
import org.springframework.cloud.logging.LoggingRebinder;
Expand All @@ -66,6 +67,7 @@
*
* @author Dave Syer
* @author Venil Noronha
* @author Olga Maciaszek-Sharma
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RefreshScope.class)
Expand Down Expand Up @@ -117,6 +119,12 @@ public ConfigDataContextRefresher configDataContextRefresher(ConfigurableApplica
return new ConfigDataContextRefresher(context, scope, properties);
}

@ConditionalOnProperty(value = "spring.cloud.refresh.on-restart.enabled", matchIfMissing = true)
@Bean
RefreshScopeLifecycle refreshScopeLifecycle(ContextRefresher contextRefresher) {
return new RefreshScopeLifecycle(contextRefresher);
}

@Bean
public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
return new RefreshEventListener(contextRefresher);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2012-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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.context.refresh;

import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.context.Lifecycle;

/**
* A {@link Lifecycle} implementation that triggers {@link ContextRefresher#refresh()} to
* be called on restart.
*
* @author Olga Maciaszek-Sharma
* @since 4.1.0
*/
public class RefreshScopeLifecycle implements Lifecycle {

private static final Log LOG = LogFactory.getLog(RefreshScopeLifecycle.class);

private final ContextRefresher contextRefresher;

private final Object lifecycleMonitor = new Object();

private volatile boolean running = true;

public RefreshScopeLifecycle(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}

@Override
public void start() {
synchronized (lifecycleMonitor) {
if (!isRunning()) {
if (LOG.isInfoEnabled()) {
LOG.info("Refreshing context on restart.");
}
Set<String> keys = contextRefresher.refresh();
if(LOG.isInfoEnabled()){
LOG.info("Refreshed keys: " + keys);
}
}
running = true;
}
}

@Override
public void stop() {
synchronized (lifecycleMonitor) {
if (isRunning()) {
running = false;
}
}
}

@Override
public boolean isRunning() {
return running;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
"type": "java.lang.Boolean",
"description": "Enable the DecryptEnvironmentPostProcessor.",
"defaultValue": true
},
{
"name": "spring.cloud.refresh.on-restart.enabled",
"type": "java.lang.Boolean",
"description": "Enable refreshing context on start.",
"defaultValue": true
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.cloud.context.refresh.ContextRefresher;
Expand All @@ -38,9 +40,10 @@

/**
* @author Dave Syer
* @author Olga Maciaszek-Sharma
*/
@ExtendWith(OutputCaptureExtension.class)
public class RefreshAutoConfigurationTests {
class RefreshAutoConfigurationTests {

private static ConfigurableApplicationContext getApplicationContext(WebApplicationType type, Class<?> configuration,
String... properties) {
Expand All @@ -49,23 +52,23 @@ private static ConfigurableApplicationContext getApplicationContext(WebApplicati
}

@Test
public void noWarnings(CapturedOutput output) {
void noWarnings(CapturedOutput output) {
try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class)) {
then(context.containsBean("refreshScope")).isTrue();
then(output.toString()).doesNotContain("WARN");
}
}

@Test
public void disabled() {
void disabled() {
try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.SERVLET, Config.class,
"spring.cloud.refresh.enabled:false")) {
then(context.containsBean("refreshScope")).isFalse();
}
}

@Test
public void refreshables() {
void refreshables() {
try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class,
"config.foo=bar", "spring.cloud.refresh.refreshable:" + SealedConfigProps.class.getName())) {
context.getBean(SealedConfigProps.class);
Expand All @@ -84,7 +87,7 @@ public void extraRefreshables() {
}

@Test
public void neverRefreshable() {
void neverRefreshable() {
try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class,
"countingconfig.foo=bar",
"spring.cloud.refresh.never-refreshable:" + CountingConfigProps.class.getName())) {
Expand All @@ -94,6 +97,19 @@ public void neverRefreshable() {
}
}

@Test
void refreshScopeLifecylePresentByDefault() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class))
.run(context -> assertThat(context).hasBean("refreshScopeLifecycle"));
}

@Test
void refreshScopeLifecyleDisabledWithProp() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class))
.withPropertyValues("spring.cloud.refresh.on-restart.enabled=false")
.run(context -> assertThat(context).doesNotHaveBean("refreshScopeLifecycle"));
}

@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
@EnableConfigurationProperties({ SealedConfigProps.class, CountingConfigProps.class })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2012-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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.context.refresh;

import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

/**
* Tests for {@link RefreshScopeLifecycle}.
*
* @author Olga Maciaszek-Sharma
*/
class RefreshScopeLifecycleTests {

ContextRefresher contextRefresher = mock();

private final RefreshScopeLifecycle lifecycle = new RefreshScopeLifecycle(contextRefresher);

@Test
void shouldRefreshContextOnRestart() {
lifecycle.stop();
lifecycle.start();

verify(contextRefresher).refresh();
}

@Test
void shouldNotRefreshContextOnStart() {
lifecycle.start();

verifyNoInteractions(contextRefresher);
}

}