I have a Spring Boot application that is using Zuul w/Ribbon that is not forwarding requests to newly updated listOfServer destinations when that destination is a placeholder. For context, the application gets its config from a spring cloud config server and is configured to send .yml files and .properties files.
I am attempting to refresh the listOfServer using the refresh actuator endpoint.
Sample .yml file:
zuul:
routes:
foo:
path: /bar
serviceId: foo
foo:
ribbon:
listOfServers: ${foo_servers}
Sample .properties file:
foo_servers=localhost:8443
Sample Response from Cloud-Config
{
name: "foobar",
profiles: [
"x, y, z"
],
label: "local-dev",
version: null,
state: null,
propertySources: [
{
name: "application.yml",
source: {
zuul.routes.foo.path="/bar"
zuul.routes.foo.serviceId="foo"
foo.ribbon.listOfServers="${foo_servers}"
}
},
{
name: "application.properties",
source: {
foo_servers="localhost:9443"
}
]
}
Observed behavior:
1. When I change the .yml file from ${foo_servers} to localhost:9443, it works as expected. Requests to /bar are forwarded to the updated localhost:9443 location.
2. When I update foo_servers, I restart the spring cloud config server before refreshing the spring boot application. I can see that an event is triggered for updating foo_servers, but requests to /bar will still map to the previous value of foo_servers. I can see the updated value on the env actuator page even though requests are being sent to 8443, e.g.
foo.ribbon.listOfServers: "localhost:9443"
How can I get Ribbon to accept the updated placeholder value for listOfServers?
Comment From: ryanjbaxter
What version of spring cloud?
Comment From: goliath5811
I am using Dalston.SR4 (1.3.3.RELEASE)
Comment From: ryanjbaxter
Please try with Edgware.SR4 or Finchley.SR1.
Comment From: goliath5811
The same issue is in Edgware.SR4. I was not able to test Finchley.SR1 because my applications are not able to upgrade to Spring Boot 2.0 at this time.
Comment From: ryanjbaxter
Can you provide a project that reproduces the issue?
Comment From: goliath5811
Please expand SPR-3219.tar.gz and view the Readme.md for instructions.
Comment From: OlgaMaciaszek
@goliath5811 Thanks for the demo files and exact readme. I was able to reproduce it. I'm now verifying why it happens and if it makes sense to change it.
Comment From: OlgaMaciaszek
So what happens is the following: when we have a value with a placeholder in the config server like so: listOfServers: ${foo_servers}, whenever someone changes the placeholder, in ContextRefresher, we obviously determine that it's been the foo_servers property key whose value has changed. An EnvironmentChangeEvent is published for that change, and subsequently a ConfigurationEvent is passed to ArchaiusConfigurationListenerAdapter that tries to find the property by the key and update it, but since the passed key is foo_servers and the actual ribbon config key is listOfServers nothing happens. We will now have a look at possible solutions and discuss if it makes sense to apply them and get back to you. @goliath5811 thanks again for reporting it.
Comment From: goliath5811
Thanks so much for looking into this @OlgaMaciaszek
Comment From: OlgaMaciaszek
@goliath5811 I don't think we are going to be fixing passing keys to Archaius config at this point, but we can offer the following workaround:
In the config repo in demo-gateway.yml:
foo:
foo_servers: ${foo_servers}
Then in the gateway app, pass a custom ribbon config, like so:
@EnableZuulProxy
@EnableCircuitBreaker
@SpringBootApplication
@RibbonClients(defaultConfiguration = RibbonClientConfiguration.class)
public class DemoGatewayApplication {...}
And use the property with placeholder directly in that config to build the list of servers:
@Configuration
public class RibbonClientConfiguration {
@Bean
@RefreshScope // necessary to get the server list values refreshed via the `refresh` post
public ServerList<Server> serverList(@Value("${foo.foo_servers}") String fooServers) {
return new ServerList<Server>() {
@Override
public List<Server> getInitialListOfServers() {
return getServers(fooServers);
}
@Override
public List<Server> getUpdatedListOfServers() {
return getServers(fooServers);
}
};
}
// take the resolved placeholder property value and build server list using that
private List<Server> getServers(String serverInfo) {
...
return servers;
}
Comment From: OlgaMaciaszek
@goliath5811 Please let me know if this workaround solves your issue.
Comment From: goliath5811
Thanks for providing the workaround. Would I need to define Ribbon client configuration for each route that currently uses placeholders for it's listOfServers yml configuration? From looking at the documentation, it looks like I would need to map the new config to a route by name.
Comment From: OlgaMaciaszek
@goliath5811 this config is general, passed on application level via @RibbonClients, so it should work for all the ribbon clients that are under the scope of that config. Do you have any more issues with this? If yes, please provide an example.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.
Comment From: k-tomaszewski
I've faced the same issue. Couple of tests using @SpringBootTest but different configurations caused couple of separate Spring contexts being created. Unfortunately Ribbon configuration, precisely "listOfServers", stayed the same after the first such test and it was not updated during subsequent tests using other Spring contexts.
I found following workaround: in the test class using @SpringBootTest and having issues caused by invalid Ribbon configuration (wrong "listOfServers") one needs to:
1) add autowired field ApplicationContext appContext to the test class
2) run this piece of code before a test - it can be done in a separate method annotated with @BeforeEach from JUnit5:
ConfigurationManager.getConfigInstance().clear();
appContext.getBean(ArchaiusAutoConfiguration.class).close();
ArchaiusAutoConfiguration.configurableEnvironmentConfiguration(appContext.getBean(ConfigurableEnvironment.class), appContext);
appContext.getBean(SpringClientFactory.class).destroy();
This will force Ribbon configuration to be truly reloaded. At least it worked for me with Spring Boot 2.1.9 (and 2.1.17) and Spring Cloud 2.1.3.
My conclusion: the problem was caused by the class com.netflix.config.ConfigurationManager that keeps a state in a global variable (static fields...) or maybe rather it was caused by lack of resetting this global state by Spring Cloud internals.
@OlgaMaciaszek Maybe this workaround can be some inspiration to provide an improvement in Spring beans being an integration layer for Feign/Ribbon?