From 61c8c05b7027514a8a2412bed5f67660451ab988 Mon Sep 17 00:00:00 2001 From: 98001yash Date: Thu, 29 Oct 2026 11:49:18 +0530 Subject: [PATCH] Add consistency mode to Consul KV queries Signed-off-by: 98001yash --- docs/modules/ROOT/partials/_configprops.adoc | 3 +- .../cloud/consul/config/ConfigWatch.java | 4 +- .../consul/config/ConsulConfigProperties.java | 16 ++++++ .../consul/config/ConsulPropertySource.java | 4 +- .../consul/config/ConsulPropertySources.java | 3 +- .../cloud/consul/config/ConfigWatchTests.java | 7 +-- .../config/ConsulConfigPropertiesTests.java | 18 +++++++ .../config/ConsulPropertySourcesTests.java | 8 ++- .../cloud/consul/ConsulClient.java | 53 +++++++++++++++++-- .../QueryParamsArgumentResolverTests.java | 9 ++++ 10 files changed, 110 insertions(+), 15 deletions(-) diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 85e0a290d..544e9851c 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -6,6 +6,7 @@ |spring.cloud.consul.config.default-context | `+++application+++` | |spring.cloud.consul.config.enabled | `+++true+++` | |spring.cloud.consul.config.fail-fast | `+++true+++` | Throw exceptions during config lookup if true, otherwise, log warnings. +|spring.cloud.consul.config.consistency-mode | `+++DEFAULT+++` | Consistency mode for Consul KV requests. Supported values are `DEFAULT`, `STALE`, and `CONSISTENT`. |spring.cloud.consul.config.format | `+++key-value+++` | |spring.cloud.consul.config.name | | Alternative to spring.application.name to use in looking up values in consul KV. |spring.cloud.consul.config.prefix | | @@ -81,4 +82,4 @@ |spring.cloud.consul.tls.key-store-password | | Password to an external keystore. |spring.cloud.consul.tls.key-store-path | | Path to an external keystore. -|=== \ No newline at end of file +|=== diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java index 68821e23e..4aec84d2f 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.consul.ConsulClient; +import org.springframework.cloud.consul.ConsulClient.QueryParams; import org.springframework.cloud.consul.model.http.ConsulHeaders; import org.springframework.cloud.consul.model.http.kv.GetValue; import org.springframework.cloud.endpoint.event.RefreshEvent; @@ -157,7 +158,8 @@ public void watchConfigKeyValues() { } ResponseEntity> response = this.consul.getKVValues(context, aclToken, - (long) properties.getWatch().getWaitTime(), currentIndex); + new QueryParams(this.properties.getConsistencyMode(), + (long) this.properties.getWatch().getWaitTime(), currentIndex)); // if response.value == null, response was a 404, otherwise it was a // 200, reducing churn if there wasn't anything diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java index fe21db36d..c7e9aec85 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.cloud.consul.ConsulClient.ConsistencyMode; import org.springframework.core.style.ToStringCreator; import org.springframework.util.CollectionUtils; import org.springframework.validation.annotation.Validated; @@ -68,6 +69,12 @@ public class ConsulConfigProperties { @Value("${consul.token:${CONSUL_TOKEN:${spring.cloud.consul.token:${SPRING_CLOUD_CONSUL_TOKEN:}}}}") private String aclToken; + /** + * Consistency mode for Consul KV requests. + */ + @NotNull + private ConsistencyMode consistencyMode = ConsistencyMode.DEFAULT; + private Watch watch = new Watch(); /** @@ -165,6 +172,14 @@ public void setAclToken(String aclToken) { this.aclToken = aclToken; } + public ConsistencyMode getConsistencyMode() { + return this.consistencyMode; + } + + public void setConsistencyMode(ConsistencyMode consistencyMode) { + this.consistencyMode = consistencyMode; + } + public Watch getWatch() { return this.watch; } @@ -198,6 +213,7 @@ public String toString() { .append("format", this.format) .append("dataKey", this.dataKey) .append("aclToken", this.aclToken != null ? "******" : null) + .append("consistencyMode", this.consistencyMode) .append("watch", this.watch) .append("failFast", this.failFast) .append("name", this.name) diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySource.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySource.java index ddf15f30b..9a36857e0 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySource.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySource.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.cloud.consul.ConsulClient; +import org.springframework.cloud.consul.ConsulClient.QueryParams; import org.springframework.cloud.consul.model.http.ConsulHeaders; import org.springframework.cloud.consul.model.http.kv.GetValue; import org.springframework.core.env.EnumerablePropertySource; @@ -55,7 +56,6 @@ public ConsulPropertySource(String context, ConsulClient source, ConsulConfigPro super(context, source); this.context = context; this.configProperties = configProperties; - } public void init() { @@ -70,7 +70,7 @@ public void init() { } ResponseEntity> response = this.source.getKVValues(this.context, - this.configProperties.getAclToken()); + this.configProperties.getAclToken(), new QueryParams(this.configProperties.getConsistencyMode())); this.initialIndex = ConsulHeaders.getConsulIndex(response); diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java index 17e9f375d..2736f7701 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.springframework.cloud.consul.ConsulClient; +import org.springframework.cloud.consul.ConsulClient.QueryParams; import org.springframework.cloud.consul.ConsulException; import org.springframework.cloud.consul.model.http.ConsulHeaders; import org.springframework.cloud.consul.model.http.kv.GetValue; @@ -127,7 +128,7 @@ public ConsulPropertySource createPropertySource(String propertySourceContext, C if (properties.getFormat() == FILES) { ResponseEntity> response = consul.getKVValue(propertySourceContext, - properties.getAclToken()); + properties.getAclToken(), new QueryParams(properties.getConsistencyMode())); GetValue value = null; if (response.getStatusCode().is2xxSuccessful()) { diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java index 60d16ec92..4feaa8df4 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.springframework.cloud.consul.ConsulClient; +import org.springframework.cloud.consul.ConsulClient.QueryParams; import org.springframework.cloud.consul.model.http.ConsulHeaders; import org.springframework.cloud.consul.model.http.kv.GetValue; import org.springframework.cloud.endpoint.event.RefreshEvent; @@ -36,7 +37,6 @@ import org.springframework.util.StringUtils; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; @@ -119,7 +119,8 @@ private void setupWatch(ApplicationEventPublisher eventPublisher, GetValue getVa headers.add(ConsulHeaders.INDEX_HEADER, "1"); headers.add(ConsulHeaders.LAST_CONTACT_HEADER, "1"); response.headers(headers); - when(consul.getKVValues(eq(context), nullable(String.class), anyLong(), anyLong())) + + when(consul.getKVValues(eq(context), nullable(String.class), any(QueryParams.class))) .thenReturn(response.body(getValues)); if (StringUtils.hasText(aclToken)) { @@ -146,7 +147,7 @@ public void firstCallDoesNotPublishEvent() { List getValues = Collections.singletonList(getValue); ResponseEntity> response = ResponseEntity.ok(getValues); - when(consul.getKVValues(eq(context), anyString(), anyLong(), anyLong())).thenReturn(response); + when(consul.getKVValues(eq(context), anyString(), any(QueryParams.class))).thenReturn(response); ConfigWatch watch = new ConfigWatch(this.configProperties, consul, new LinkedHashMap<>()); watch.setApplicationEventPublisher(eventPublisher); diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigPropertiesTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigPropertiesTests.java index 7b7f7374e..c359d356e 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigPropertiesTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigPropertiesTests.java @@ -18,6 +18,8 @@ import org.junit.jupiter.api.Test; +import org.springframework.cloud.consul.ConsulClient.ConsistencyMode; + import static org.assertj.core.api.Assertions.assertThat; public class ConsulConfigPropertiesTests { @@ -29,4 +31,20 @@ public void aclTokenToStringMasked() { assertThat(properties.toString()).doesNotContain("myAclToken").contains("******"); } + @Test + public void consistencyModeDefaultsToDefault() { + ConsulConfigProperties properties = new ConsulConfigProperties(); + + assertThat(properties.getConsistencyMode()).isEqualTo(ConsistencyMode.DEFAULT); + } + + @Test + public void consistencyModeCanBeConfigured() { + ConsulConfigProperties properties = new ConsulConfigProperties(); + + properties.setConsistencyMode(ConsistencyMode.CONSISTENT); + + assertThat(properties.getConsistencyMode()).isEqualTo(ConsistencyMode.CONSISTENT); + } + } diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourcesTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourcesTests.java index 627abf33d..f7301fd97 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourcesTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourcesTests.java @@ -24,12 +24,14 @@ import org.junit.Test; import org.springframework.cloud.consul.ConsulClient; +import org.springframework.cloud.consul.ConsulClient.QueryParams; import org.springframework.cloud.consul.model.http.kv.GetValue; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; @@ -50,7 +52,8 @@ public class ConsulPropertySourcesTests { public void createPropertySourceStoresNormalizedContextInIndex() { ConsulClient consul = mock(ConsulClient.class); ResponseEntity> response = new ResponseEntity>(HttpStatus.OK); - when(consul.getKVValues(eq("test/"), nullable(String.class))).thenReturn(response); + + when(consul.getKVValues(eq("test/"), nullable(String.class), any(QueryParams.class))).thenReturn(response); ConsulConfigProperties properties = new ConsulConfigProperties(); ConsulPropertySources sources = new ConsulPropertySources(properties, @@ -70,7 +73,8 @@ public void createPropertySourceFilesFormatStoresNormalizedContextInIndex() { when(response.getStatusCode()).thenReturn(HttpStatus.OK); when(response.getBody()).thenReturn(Collections.emptyList()); when(response.getHeaders()).thenReturn(new HttpHeaders()); - when(consul.getKVValue(eq("test.yml"), nullable(String.class))).thenReturn(response); + + when(consul.getKVValue(eq("test.yml"), nullable(String.class), any(QueryParams.class))).thenReturn(response); ConsulConfigProperties properties = new ConsulConfigProperties(); properties.setFormat(ConsulConfigProperties.Format.FILES); diff --git a/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulClient.java b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulClient.java index 0761c9e30..9a05e8534 100644 --- a/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulClient.java +++ b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulClient.java @@ -115,14 +115,44 @@ ResponseEntity> getHealthServices(@PathVariable String servi ResponseEntity deleteKVValues(@PathVariable String context, @RequestHeader(name = ACL_TOKEN_HEADER, required = false) String aclToken); + /** + * Retrieves a single KV entry using the default query parameters. + */ @GetExchange("/v1/kv/{context}") ResponseEntity> getKVValue(@PathVariable String context, @RequestHeader(name = ACL_TOKEN_HEADER, required = false) String aclToken); + /** + * Retrieves a single KV entry using the supplied query parameters. + */ + @GetExchange("/v1/kv/{context}") + ResponseEntity> getKVValue(@PathVariable String context, + @RequestHeader(name = ACL_TOKEN_HEADER, required = false) String aclToken, QueryParams queryParams); + + /** + * Retrieves KV entries recursively using the default query parameters. + */ @GetExchange("/v1/kv/{context}?recurse") ResponseEntity> getKVValues(@PathVariable String context, @RequestHeader(name = ACL_TOKEN_HEADER, required = false) String aclToken); + /** + * Retrieves KV entries recursively using the supplied query parameters. + * + * The QueryParams argument allows callers to specify the datacenter, consistency + * mode, and other supported Consul query parameters. + */ + @GetExchange("/v1/kv/{context}?recurse") + ResponseEntity> getKVValues(@PathVariable String context, + @RequestHeader(name = ACL_TOKEN_HEADER, required = false) String aclToken, QueryParams queryParams); + + /** + * Retrieves KV entries recursively using blocking queries. + * + * This existing method is retained for compatibility. Callers requiring a consistency + * mode for blocking queries should use the QueryParams overload and include waitTime + * and index in QueryParams. + */ @GetExchange("/v1/kv/{context}?recurse") ResponseEntity> getKVValues(@PathVariable String context, @RequestHeader(name = ACL_TOKEN_HEADER, required = false) String aclToken, @@ -150,6 +180,9 @@ ResponseEntity> eventList(@RequestParam("wait") @WaitTimeFormat Long ResponseEntity eventFire(@PathVariable String name, @RequestBody String payload, @RequestHeader(name = ACL_TOKEN_HEADER, required = false) String aclToken); + /** + * Query parameters supported by Consul HTTP API requests. + */ class QueryParams { /** @@ -187,6 +220,13 @@ public QueryParams(long waitTime, long index) { this(null, ConsistencyMode.DEFAULT, waitTime, index); } + /** + * Creates query parameters with a consistency mode and blocking-query settings. + */ + public QueryParams(ConsistencyMode consistencyMode, long waitTime, long index) { + this(null, consistencyMode, waitTime, index); + } + public QueryParams(String datacenter, long waitTime, long index) { this(datacenter, ConsistencyMode.DEFAULT, waitTime, index, null); } @@ -226,31 +266,34 @@ public String getNear() { } + /** + * Consistency modes supported by the Consul HTTP API. + */ enum ConsistencyMode { /** - * Default Consul Consistency Mode. + * Default Consul consistency mode. */ DEFAULT("default"), /** - * Stale Consul Consistency Mode. + * Stale Consul consistency mode. */ STALE("stale"), /** - * Consistent Consul Consistency Mode. + * Consistent Consul consistency mode. */ CONSISTENT("consistent"); - private String paramName; + private final String paramName; ConsistencyMode(String paramName) { this.paramName = paramName; } public String getParamName() { - return paramName; + return this.paramName; } @Override diff --git a/spring-cloud-consul-core/src/test/java/org/springframework/cloud/consul/QueryParamsArgumentResolverTests.java b/spring-cloud-consul-core/src/test/java/org/springframework/cloud/consul/QueryParamsArgumentResolverTests.java index 0b04b9f4c..cee0607cd 100644 --- a/spring-cloud-consul-core/src/test/java/org/springframework/cloud/consul/QueryParamsArgumentResolverTests.java +++ b/spring-cloud-consul-core/src/test/java/org/springframework/cloud/consul/QueryParamsArgumentResolverTests.java @@ -61,6 +61,15 @@ private URI resolve(QueryParams params) throws Exception { values.getUriVariables()); } + @Test + public void consistentModePreservesBlockingQueryParameters() throws Exception { + URI uri = resolve(new QueryParams(ConsistencyMode.CONSISTENT, 30, 123)); + + assertThat(uri.getQuery()).contains("consistent="); + assertThat(uri.getQuery()).contains("wait=30s"); + assertThat(uri.getQuery()).contains("index=123"); + } + void target(QueryParams params) { }