Skip to content

Skip route refresh after the context starts shutting down#4160

Open
PreAgile wants to merge 1 commit into
spring-cloud:mainfrom
PreAgile:fix/issue-2471-skip-refresh-during-shutdown
Open

Skip route refresh after the context starts shutting down#4160
PreAgile wants to merge 1 commit into
spring-cloud:mainfrom
PreAgile:fix/issue-2471-skip-refresh-during-shutdown

Conversation

@PreAgile
Copy link
Copy Markdown
Contributor

@PreAgile PreAgile commented May 4, 2026

Summary

Stops CachingRouteLocator from delegating to the wrapped RouteLocator once the ApplicationContext has begun closing. The listener was firing on RefreshRoutesEvent instances published during the destroy phase (typically by ConsulServiceRegistry.deregister()), which led the delegate path to ask the BeanFactory for beans that were already being destroyed and produced the BeanCreationNotAllowedException reported in gh-2471.

Why isActive() is not enough

The first instinct is AbstractApplicationContext.isActive(), but active only flips to false at the very end of doClose() — after destroyBeans() has already run. The first signal available is ContextClosedEvent, published in step 1 of doClose(), before any destroy callback fires.

publishEvent(ContextClosedEvent)   // step 1
getLifecycleProcessor().onClose()  // step 2
destroyBeans()                     // step 3  ← failure happens here
...
this.active.set(false)             // step 6

The fix

When the event publisher is a ConfigurableApplicationContext, register a small ContextClosedEvent listener that flips a volatile boolean contextClosing flag, and short-circuit onApplicationEvent(RefreshRoutesEvent) when the flag is set.

public void onApplicationEvent(RefreshRoutesEvent event) {
    if (contextClosing) { return; }
    // existing logic
}

public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
    this.applicationEventPublisher = publisher;
    if (publisher instanceof ConfigurableApplicationContext context) {
        context.addApplicationListener(
            (ApplicationListener<ContextClosedEvent>) event -> this.contextClosing = true);
    }
}

+12 / -2 in CachingRouteLocator.java. No interfaces added. The existing CachingRouteLocatorTests keep passing because they wire a lambda publisher, so the new instanceof branch is skipped and the guard stays inactive — the original behaviour is preserved for non-context publishers.

Reproduction

CachingRouteLocatorShutdownTests makes the race deterministic by registering a DisposableBean whose destroy() publishes a RefreshRoutesEvent — the same timing the Consul registry exercises:

context.getDefaultListableBeanFactory().registerDisposableBean(
    "refreshOnShutdown",
    () -> context.publishEvent(new RefreshRoutesEvent(this)));
context.close();

Without the guard the test fails (expected: 1 but was: 2); with the guard it passes. The broader Route/Routing test set in spring-cloud-gateway-server-webflux (219 tests) stays green.

Happy to open a 4.3.x backport once this lands.

Fixes gh-2471

CachingRouteLocator listens for RefreshRoutesEvent and delegates to its
RouteLocator on every event. RefreshRoutesEvent can be published during
the destroy phase of the ApplicationContext -- for example,
ConsulServiceRegistry.deregister() runs as a destroy callback and emits
one, at which point any bean lookup performed transitively by the
delegate fails with BeanCreationNotAllowedException, as in the original
report (RouteDefinitionRouteLocator requesting `defaultValidator` to
validate RouteDefinitions).

AbstractApplicationContext.isActive() cannot guard this path because
active is only set to false at the very end of doClose(), after
destroyBeans() has already run. The first signal that close has begun
is ContextClosedEvent, which doClose() publishes before destroyBeans().

Register a one-shot ContextClosedEvent listener on the
ApplicationEventPublisher when it is a ConfigurableApplicationContext,
flip a volatile flag from it, and short-circuit
onApplicationEvent(RefreshRoutesEvent) when the flag is set.

CachingRouteLocatorShutdownTests reproduces the issue deterministically
by registering a DisposableBean whose destroy() publishes a
RefreshRoutesEvent -- the same timing exercised by the Consul registry
shutdown path. Existing CachingRouteLocatorTests keep passing because
they wire a lambda as the event publisher, so the new instanceof branch
is skipped and the guard remains inactive, preserving the original
behaviour for non-context publishers.

Fixes spring-cloudgh-2471

Signed-off-by: Meyon Soo  Kim <39788337+PreAgile@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

@PreAgile PreAgile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shutdown throw BeanCreationNotAllowedException

2 participants