diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java index bbf842075..322b24c05 100644 --- a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java @@ -18,6 +18,7 @@ import java.util.List; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -30,6 +31,7 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.cloud.bus.BusProperties; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.config.server.environment.JGitEnvironmentRepository; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -115,11 +117,12 @@ protected static class PropertyPathEndpointConfiguration { @Bean public PropertyPathEndpoint propertyPathEndpoint(List notifiers, - MonitorConfigurationProperties monitorProperties) { + MonitorConfigurationProperties monitorProperties, + ObjectProvider gitRepositories) { Assert.state(!notifiers.isEmpty(), "At least one PropertyPathNotifier must be available"); return new PropertyPathEndpoint(new CompositePropertyPathNotificationExtractor(this.extractors), notifiers, monitorProperties.getMaxDashes(), monitorProperties.getMaxPaths(), - monitorProperties.getIgnoredPaths()); + monitorProperties.getIgnoredPaths(), gitRepositories.orderedStream().toList()); } } diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java index 80ea54a2c..818e9cac9 100644 --- a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.config.server.environment.JGitEnvironmentRepository; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; @@ -71,6 +72,13 @@ public class PropertyPathEndpoint { private final List ignoredPaths; + /** + * Git repositories of this config server. Their refresh rate is expired before the + * applications are notified, so that the configuration the applications fetch on + * refresh includes the change that triggered the notification. + */ + private final List gitRepositories; + public PropertyPathEndpoint(PropertyPathNotificationExtractor extractor, PropertyPathNotifier notifier) { this(extractor, List.of(notifier), MonitorConfigurationProperties.DEFAULT_MAX_DASHES); } @@ -88,7 +96,13 @@ public PropertyPathEndpoint(PropertyPathNotificationExtractor extractor, List notifiers, int maxDashes, int maxPaths, List ignoredPaths) { + this(extractor, notifiers, maxDashes, maxPaths, ignoredPaths, Collections.emptyList()); + } + + public PropertyPathEndpoint(PropertyPathNotificationExtractor extractor, List notifiers, + int maxDashes, int maxPaths, List ignoredPaths, List gitRepositories) { this.extractor = extractor; + this.gitRepositories = gitRepositories == null ? Collections.emptyList() : gitRepositories; this.notifiers = notifiers; this.maxDashes = maxDashes; this.maxPaths = maxPaths; @@ -121,6 +135,9 @@ public Set notifyByPath(@RequestHeader HttpHeaders headers, @RequestBody for (String service : services) { log.info("Refresh for: " + service); } + for (JGitEnvironmentRepository repository : this.gitRepositories) { + repository.expireRefreshRate(); + } for (PropertyPathNotifier notifier : this.notifiers) { notifier.notifyApplications(services); } diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java index 667aaab63..f29ea37fc 100644 --- a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java @@ -22,8 +22,12 @@ import java.util.List; import java.util.Set; +import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; +import org.springframework.cloud.config.server.environment.JGitEnvironmentProperties; +import org.springframework.cloud.config.server.environment.JGitEnvironmentRepository; +import org.springframework.core.env.StandardEnvironment; import org.springframework.http.HttpHeaders; import static org.assertj.core.api.Assertions.assertThat; @@ -63,6 +67,37 @@ public void testNotifiesAffectedServices() { assertThat(notifiedServices).containsExactly("foo"); } + @Test + public void testExpiresGitRefreshRateBeforeNotifyingServices() { + JGitEnvironmentRepository repository = gitRepository(); + repository.setLastRefresh(System.currentTimeMillis()); + List lastRefreshWhenNotified = new ArrayList<>(); + PropertyPathNotifier notifier = services -> lastRefreshWhenNotified.add(repository.getLastRefresh()); + PropertyPathEndpoint endpoint = new PropertyPathEndpoint( + new CompositePropertyPathNotificationExtractor(Collections.emptyList()), List.of(notifier), + MonitorConfigurationProperties.DEFAULT_MAX_DASHES, MonitorConfigurationProperties.DEFAULT_MAX_PATHS, + Collections.emptyList(), List.of(repository)); + + endpoint.notifyByPath(new HttpHeaders(), Collections.singletonMap("path", "foo.yml")); + + assertThat(lastRefreshWhenNotified).containsExactly(0L); + } + + @Test + public void testDoesNotExpireGitRefreshRateWhenNoServiceIsAffected() { + JGitEnvironmentRepository repository = gitRepository(); + long lastRefresh = System.currentTimeMillis(); + repository.setLastRefresh(lastRefresh); + PropertyPathEndpoint endpoint = new PropertyPathEndpoint( + new CompositePropertyPathNotificationExtractor(Collections.emptyList()), List.of(services -> { + }), MonitorConfigurationProperties.DEFAULT_MAX_DASHES, MonitorConfigurationProperties.DEFAULT_MAX_PATHS, + List.of("foo\\.yml"), List.of(repository)); + + endpoint.notifyByPath(new HttpHeaders(), Collections.singletonMap("path", "foo.yml")); + + assertThat(repository.getLastRefresh()).isEqualTo(lastRefresh); + } + @Test public void testNotifyAll() { assertThat(this.endpoint.notifyByPath(new HttpHeaders(), Collections.singletonMap("path", "application.yml")) @@ -145,4 +180,10 @@ public void testNotifyLimitsPaths() { assertThat(limitedEndpoint.notifyByForm(new HttpHeaders(), request).toString()).isEqualTo("[foo, bar]"); } + private static JGitEnvironmentRepository gitRepository() { + JGitEnvironmentProperties properties = new JGitEnvironmentProperties(); + properties.setRefreshRate(60); + return new JGitEnvironmentRepository(new StandardEnvironment(), properties, ObservationRegistry.NOOP); + } + } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java index a5aaaf8c9..78dce68a4 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java @@ -907,6 +907,15 @@ public void setLastRefresh(long lastRefresh) { this.lastRefresh = lastRefresh; } + /** + * Makes the next request fetch from the remote repository, even if the + * {@link #getRefreshRate() refresh rate} has not elapsed since the last fetch. Has no + * effect if the refresh rate is negative, in which case the remote is never fetched. + */ + public void expireRefreshRate() { + this.lastRefresh = 0; + } + /** * Wraps the static method calls to {@link org.eclipse.jgit.api.Git} and * {@link org.eclipse.jgit.api.CloneCommand} allowing for easier unit testing. diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepository.java index 80be4a4fd..71e357107 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepository.java @@ -119,6 +119,13 @@ public void setRepos(Map repos this.repos.putAll(repos); } + @Override + public void expireRefreshRate() { + super.expireRefreshRate(); + this.repos.values().forEach(JGitEnvironmentRepository::expireRefreshRate); + new ArrayList<>(this.placeholders.values()).forEach(JGitEnvironmentRepository::expireRefreshRate); + } + @Override public Locations getLocations(String application, String profile, String label) { for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) { diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java index 4d54707a1..57691f3a2 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java @@ -496,6 +496,37 @@ public void testNewCommitID() throws Exception { assertThat("bar").isEqualTo(fooProperty); } + @Test + public void expireRefreshRateFetchesNewCommitWithinRefreshRate() throws Exception { + JGitConfigServerTestData testData = JGitConfigServerTestData + .prepareClonedGitRepository(TestConfiguration.class); + JGitEnvironmentRepository repository = testData.getRepository(); + repository.setRefreshRate(60); + + // the first request fetches and starts the refresh rate window + Environment environment = repository.findOne("bar", "staging", "master"); + String startingRemoteVersion = getCommitID(testData.getServerGit().getGit(), "master"); + assertThat(environment.getVersion()).isEqualTo(startingRemoteVersion); + + // update the remote repo + FileOutputStream out = new FileOutputStream( + new File(testData.getServerGit().getGitWorkingDirectory(), "bar.properties")); + StreamUtils.copy("foo: barNewCommit", Charset.defaultCharset(), out); + testData.getServerGit().getGit().add().addFilepattern("bar.properties").call(); + testData.getServerGit().getGit().commit().setMessage("Updated for pull").call(); + String updatedRemoteVersion = getCommitID(testData.getServerGit().getGit(), "master"); + + // within the refresh rate window the new commit is not fetched + environment = repository.findOne("bar", "staging", "master"); + assertThat(environment.getVersion()).isEqualTo(startingRemoteVersion); + + repository.expireRefreshRate(); + + environment = repository.findOne("bar", "staging", "master"); + assertThat(environment.getVersion()).isEqualTo(updatedRemoteVersion); + assertThat(ConfigServerTestUtils.getProperty(environment, "bar.properties", "foo")).isEqualTo("barNewCommit"); + } + @Test /** * In this scenario there is set the refresh rate so the remote repository is not diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryTests.java index 9f0b13f5e..b2881e3a6 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MultipleJGitEnvironmentRepositoryTests.java @@ -88,6 +88,19 @@ private PatternMatchingJGitEnvironmentRepository createRepository(String name, S return repo; } + @Test + public void expireRefreshRateExpiresAllRepositories() { + long now = System.currentTimeMillis(); + this.repository.setLastRefresh(now); + this.repository.getRepos().values().forEach(repo -> repo.setLastRefresh(now)); + + this.repository.expireRefreshRate(); + + assertThat(this.repository.getLastRefresh()).isZero(); + assertThat(this.repository.getRepos()).isNotEmpty(); + assertThat(this.repository.getRepos().values()).allSatisfy(repo -> assertThat(repo.getLastRefresh()).isZero()); + } + @Test public void defaultRepo() { Environment environment = this.repository.findOne("bar", "staging", "master");