Skip to content

Commit

Permalink
Deregister failed contexts from SpringApplicationShutdownHook
Browse files Browse the repository at this point in the history
Prior to this change, SpringApplication would register contexts to
SpringApplicationShutdownHook and only deregister them when they're
properly closed. A failed refresh attempt does not deregister the
context from the shutdown hook.
When a test suite runs lots of tests failing because of failed contexts,
this can build up and consume lots of resources.

This commit fixes this leak and deregisters failed contexts.

Fixes gh-29874
  • Loading branch information
bclozel committed Feb 18, 2022
1 parent c676b8b commit 00114f9
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@ private void handleRunFailure(ConfigurableApplicationContext context, Throwable
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
context.close();
shutdownHook.deregisterFailedApplicationContext(context);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Brian Clozel
*/
class SpringApplicationShutdownHook implements Runnable {

Expand Down Expand Up @@ -92,6 +93,17 @@ void addRuntimeShutdownHook() {
}
}

void deregisterFailedApplicationContext(ConfigurableApplicationContext applicationContext) {
synchronized (SpringApplicationShutdownHook.class) {
if (!applicationContext.isActive()) {
SpringApplicationShutdownHook.this.contexts.remove(applicationContext);
}
else {
throw new IllegalStateException("Cannot unregister active application context");
}
}
}

@Override
public void run() {
Set<ConfigurableApplicationContext> contexts;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,6 +26,7 @@
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
Expand All @@ -36,12 +37,14 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Tests for {@link SpringApplicationShutdownHook}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Brian Clozel
*/
class SpringApplicationShutdownHookTests {

Expand Down Expand Up @@ -154,6 +157,29 @@ void removeHandlerActionWhenShuttingDownThrowsException() {
.withMessage("Shutdown in progress");
}

@Test
void failsWhenDeregisterActiveContext() {
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
ConfigurableApplicationContext context = new GenericApplicationContext();
shutdownHook.registerApplicationContext(context);
context.refresh();
assertThatThrownBy(() -> shutdownHook.deregisterFailedApplicationContext(context))
.isInstanceOf(IllegalStateException.class);
assertThat(shutdownHook.isApplicationContextRegistered(context)).isTrue();
}

@Test
void deregistersFailedContext() {
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
GenericApplicationContext context = new GenericApplicationContext();
shutdownHook.registerApplicationContext(context);
context.registerBean(FailingBean.class);
assertThatThrownBy(context::refresh).isInstanceOf(BeanCreationException.class);
assertThat(shutdownHook.isApplicationContextRegistered(context)).isTrue();
shutdownHook.deregisterFailedApplicationContext(context);
assertThat(shutdownHook.isApplicationContextRegistered(context)).isFalse();
}

static class TestSpringApplicationShutdownHook extends SpringApplicationShutdownHook {

private boolean runtimeShutdownHookAdded;
Expand Down Expand Up @@ -259,4 +285,13 @@ public void afterPropertiesSet() throws Exception {

}

static class FailingBean implements InitializingBean {

@Override
public void afterPropertiesSet() throws Exception {
throw new IllegalArgumentException("test failure");
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

package org.springframework.boot;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
Expand Down Expand Up @@ -145,6 +147,7 @@
* @author Artsiom Yudovin
* @author Marten Deinum
* @author Nguyen Bao Sach
* @author Brian Clozel
*/
@ExtendWith(OutputCaptureExtension.class)
class SpringApplicationTests {
Expand Down Expand Up @@ -1285,6 +1288,20 @@ void bindsEnvironmentPrefixToSpringApplication() {
assertThat(application.getEnvironmentPrefix()).isEqualTo("my");
}

@Test
void deregistersShutdownHookForFailedApplicationContext() {
SpringApplication application = new SpringApplication(BrokenPostConstructConfig.class);
List<ApplicationEvent> events = new ArrayList<>();
application.addListeners(events::add);
application.setWebApplicationType(WebApplicationType.NONE);
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(application::run);
assertThat(events).hasAtLeastOneElementOfType(ApplicationFailedEvent.class);
ApplicationFailedEvent failure = events.stream().filter((event) -> event instanceof ApplicationFailedEvent)
.map(ApplicationFailedEvent.class::cast).findFirst().get();
assertThat(SpringApplicationShutdownHookInstance.get())
.didNotRegisterApplicationContext(failure.getApplicationContext());
}

private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
S state) {
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)
Expand Down

0 comments on commit 00114f9

Please sign in to comment.