From 3d9beb42169fc0ef1fdc7312162f702a614d8ae7 Mon Sep 17 00:00:00 2001 From: DDINGJOO <173995233+DDINGJOO@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:30:46 +0900 Subject: [PATCH] Expire Git refresh rate when the monitor endpoint is notified When spring.cloud.config.server.git.refresh-rate is set, the config server does not fetch from the remote until the refresh rate has elapsed. A webhook sent to /monitor notifies the applications, which then fetch their configuration from the config server, but the config server may still serve the previous commit to them. Add JGitEnvironmentRepository#expireRefreshRate, which makes the next request fetch from the remote. MultipleJGitEnvironmentRepository also expires the repositories it manages. PropertyPathEndpoint expires the refresh rate of the config server's Git repositories before notifying the affected applications. Fixes gh-2249 Signed-off-by: DDINGJOO <173995233+DDINGJOO@users.noreply.github.com> --- .../EnvironmentMonitorAutoConfiguration.java | 7 +++- .../config/monitor/PropertyPathEndpoint.java | 17 ++++++++ .../monitor/PropertyPathEndpointTests.java | 41 +++++++++++++++++++ .../JGitEnvironmentRepository.java | 9 ++++ .../MultipleJGitEnvironmentRepository.java | 7 ++++ ...EnvironmentRepositoryIntegrationTests.java | 31 ++++++++++++++ ...ultipleJGitEnvironmentRepositoryTests.java | 13 ++++++ 7 files changed, 123 insertions(+), 2 deletions(-) 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 bbf8420751..322b24c055 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 80ea54a2c3..818e9cac9a 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 667aaab63c..f29ea37fc5 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 a5aaaf8c96..78dce68a4f 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 80be4a4fd6..71e3571077 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 4d54707a13..57691f3a23 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 9f0b13f5e3..b2881e3a6f 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");