spring-cloud / spring-cloud/spring-cloud-gateway
CachingRouteLocator does not properly refresh routes upon RefreshRoutesEvent due to a racing condition
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 4.9k
- Forks
- 3.5k
- Avg merge
- 20h 57m
- Merged PRs (30d)
- 8
Description
Describe the bug
I have a spring cloud gateway configured to derive routes from Consul discovery client. When a service gets registered or deregistered in Consul, the gateway application receives the event, correctly identifies that there's been a configuration change and correctly fires event RefreshRoutesEvent.
As expected, listener method in CachingRouteLocator
public void onApplicationEvent(RefreshRoutesEvent event) {
try {
fetch().collect(Collectors.toList()).subscribe(
list -> Flux.fromIterable(list).materialize().collect(Collectors.toList()).subscribe(signals -> {
applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this));
cache.put(CACHE_KEY, signals);
}, this::handleRefreshError), this::handleRefreshError);
}
catch (Throwable e) {
handleRefreshError(e);
}
}
receives the event and updates the routes. However, the list of routes it receives to update cache here: cache.put(CACHE_KEY, signals); is stale. It reflects the previous configuration. So if a new service was registered, it's still missing in the cached list resulting in a missing route configuration for the newly registered service.
If I try to set a breakpoint somewhere, it starts working properly. My assumption is there's a racing condition somewhere. It could either happen locally or maybe Consul restful API has some kind of delay and does not provide latest configuration even after it has sent an update event to the gateway.
Introducing a listener that processes RefreshRoutesResultEvent fired from code above like this:
@EventListener(RefreshRoutesResultEvent.class)
public void onApplicationEvent(RefreshRoutesResultEvent event) {
if (routeLocator instanceof CachingRouteLocator) {
((CachingRouteLocator) routeLocator).refresh().subscribe(route -> {
System.out.println("Routes refreshed: " + route.getId());
});
}
}
seems to be fixing the problem. I suspect this works due to the fact that RefreshRoutesResultEvent goes to the end of the event queue and causes to delay the underlying calls to Consul (or to some kind of cache in between). The problem is that I'm not supposed to do it. Also, it may not guarantee a bulletproof fix to a racing condition. Plus it's too heavy to refresh it multiple times.
This seems to be a bug especially since slowing down a thread corrects the problem. It's not supposed to allow for racing conditions like that.
This is the version I'm currently using: spring-cloud-gateway-server-3.1.8.jar
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with CachingRouteLocator.onApplicationEvent(RefreshRoutesEvent) in the Spring Cloud Gateway version 3.1.8 context, then reproduce by registering or deregistering a service through Consul. Observe the route list passed to cache.put(CACHE_KEY, signals) and compare it with the latest discovery configuration. Done means route refreshes consistently contain newly registered services without an extra RefreshRoutesResultEvent listener.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring, spring-boot
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100