Home / Class/ SpringApplicationShutdownHookTests Class — spring-boot Architecture

SpringApplicationShutdownHookTests Class — spring-boot Architecture

Architecture documentation for the SpringApplicationShutdownHookTests class in SpringApplicationShutdownHookTests.java from the spring-boot codebase.

Entity Profile

Source Code

core/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java lines 54–344

class SpringApplicationShutdownHookTests {

	@Test
	void shutdownHookIsNotAddedUntilContextIsRegistered() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		shutdownHook.enableShutdownHookAddition();
		assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isFalse();
		ConfigurableApplicationContext context = new GenericApplicationContext();
		shutdownHook.registerApplicationContext(context);
		assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isTrue();
	}

	@Test
	void shutdownHookIsNotAddedUntilHandlerIsRegistered() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		shutdownHook.enableShutdownHookAddition();
		assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isFalse();
		shutdownHook.getHandlers().add(() -> {
		});
		assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isTrue();
	}

	@Test
	void shutdownHookIsNotAddedUntilAdditionIsEnabled() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		shutdownHook.getHandlers().add(() -> {
		});
		assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isFalse();
		shutdownHook.enableShutdownHookAddition();
		shutdownHook.getHandlers().add(() -> {
		});
		assertThat(shutdownHook.isRuntimeShutdownHookAdded()).isTrue();
	}

	@Test
	void runClosesContextsBeforeRunningHandlerActions() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		List<Object> finished = new CopyOnWriteArrayList<>();
		ConfigurableApplicationContext context = new TestApplicationContext(finished);
		shutdownHook.registerApplicationContext(context);
		context.refresh();
		Runnable handlerAction = new TestHandlerAction(finished);
		shutdownHook.getHandlers().add(handlerAction);
		shutdownHook.run();
		assertThat(finished).containsExactly(context, handlerAction);
	}

	@Test
	void runWhenContextIsBeingClosedInAnotherThreadWaitsUntilContextIsInactive() throws InterruptedException {
		// This situation occurs in the Spring Tools IDE. It triggers a context close via
		// JMX and then stops the JVM. The two actions happen almost simultaneously
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		List<Object> finished = new CopyOnWriteArrayList<>();
		CountDownLatch closing = new CountDownLatch(1);
		CountDownLatch proceedWithClose = new CountDownLatch(1);
		ConfigurableApplicationContext context = new TestApplicationContext(finished, closing, proceedWithClose);
		shutdownHook.registerApplicationContext(context);
		context.refresh();
		Runnable handlerAction = new TestHandlerAction(finished);
		shutdownHook.getHandlers().add(handlerAction);
		Thread contextThread = new Thread(context::close);
		contextThread.start();
		// Wait for context thread to begin closing the context
		closing.await();
		Thread shutdownThread = new Thread(shutdownHook);
		shutdownThread.start();
		// Shutdown thread should start waiting for context to become inactive
		Awaitility.await().atMost(Duration.ofSeconds(30)).until(shutdownThread::getState, State.TIMED_WAITING::equals);
		// Allow context thread to proceed, unblocking shutdown thread
		proceedWithClose.countDown();
		contextThread.join();
		shutdownThread.join();
		// Context should have been closed before handler action was run
		assertThat(finished).containsExactly(context, handlerAction);
	}

	@Test
	void runDueToExitDuringRefreshWhenContextHasBeenClosedDoesNotDeadlock() {
		GenericApplicationContext context = new GenericApplicationContext();
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		shutdownHook.registerApplicationContext(context);
		context.registerBean(CloseContextAndExit.class, context, shutdownHook);
		context.refresh();
	}

	@Test
	void runWhenContextIsClosedDirectlyRunsHandlerActions() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		List<Object> finished = new CopyOnWriteArrayList<>();
		ConfigurableApplicationContext context = new TestApplicationContext(finished);
		shutdownHook.registerApplicationContext(context);
		context.refresh();
		context.close();
		Runnable handlerAction1 = new TestHandlerAction(finished);
		Runnable handlerAction2 = new TestHandlerAction(finished);
		shutdownHook.getHandlers().add(handlerAction1);
		shutdownHook.getHandlers().add(handlerAction2);
		shutdownHook.run();
		assertThat(finished).contains(handlerAction1, handlerAction2);
	}

	@Test
	@SuppressWarnings("NullAway") // Test null check
	void addHandlerActionWhenNullThrowsException() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		assertThatIllegalArgumentException().isThrownBy(() -> shutdownHook.getHandlers().add(null))
			.withMessage("'action' must not be null");
	}

	@Test
	void addHandlerActionWhenShuttingDownThrowsException() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		shutdownHook.run();
		Runnable handlerAction = new TestHandlerAction(new ArrayList<>());
		assertThatIllegalStateException().isThrownBy(() -> shutdownHook.getHandlers().add(handlerAction))
			.withMessage("Shutdown in progress");
	}

	@Test
	@SuppressWarnings("NullAway") // Test null check
	void removeHandlerActionWhenNullThrowsException() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		assertThatIllegalArgumentException().isThrownBy(() -> shutdownHook.getHandlers().remove(null))
			.withMessage("'action' must not be null");
	}

	@Test
	void removeHandlerActionWhenShuttingDownThrowsException() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		Runnable handlerAction = new TestHandlerAction(new ArrayList<>());
		shutdownHook.getHandlers().add(handlerAction);
		shutdownHook.run();
		assertThatIllegalStateException().isThrownBy(() -> shutdownHook.getHandlers().remove(handlerAction))
			.withMessage("Shutdown in progress");
	}

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

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

	@Test
	void handlersRunInDeterministicOrderFromLastRegisteredToFirst() {
		TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
		Runnable r1 = mock(Runnable.class);
		Runnable r2 = mock(Runnable.class);
		Runnable r3 = mock(Runnable.class);
		shutdownHook.getHandlers().add(r2);
		shutdownHook.getHandlers().add(r1);
		shutdownHook.getHandlers().add(r3);
		shutdownHook.run();
		InOrder ordered = inOrder(r1, r2, r3);
		ordered.verify(r3).run();
		ordered.verify(r1).run();
		ordered.verify(r2).run();
		ordered.verifyNoMoreInteractions();
	}

	static class TestSpringApplicationShutdownHook extends SpringApplicationShutdownHook {

		private boolean runtimeShutdownHookAdded;

		@Override
		protected void addRuntimeShutdownHook() {
			this.runtimeShutdownHookAdded = true;
		}

		boolean isRuntimeShutdownHookAdded() {
			return this.runtimeShutdownHookAdded;
		}

	}

	static class TestApplicationContext extends AbstractApplicationContext {

		private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory();

		private final List<Object> finished;

		private final @Nullable CountDownLatch closing;

		private final @Nullable CountDownLatch proceedWithClose;

		TestApplicationContext(List<Object> finished) {
			this(finished, null, null);
		}

		TestApplicationContext(List<Object> finished, @Nullable CountDownLatch closing,
				@Nullable CountDownLatch proceedWithClose) {
			this.finished = finished;
			this.closing = closing;
			this.proceedWithClose = proceedWithClose;
		}

		@Override
		protected void refreshBeanFactory() {
		}

		@Override
		protected void closeBeanFactory() {
		}

		@Override
		protected void onClose() {
			if (this.closing != null) {
				this.closing.countDown();
			}
			if (this.proceedWithClose != null) {
				try {
					this.proceedWithClose.await(1, TimeUnit.MINUTES);
				}
				catch (InterruptedException ex) {
					Thread.currentThread().interrupt();
				}
			}
			this.finished.add(this);
		}

		@Override
		public ConfigurableListableBeanFactory getBeanFactory() {
			return this.beanFactory;
		}

	}

	static class TestHandlerAction implements Runnable {

		private final List<Object> finished;

		TestHandlerAction(List<Object> finished) {
			this.finished = finished;
		}

		@Override
		public void run() {
			this.finished.add(this);
		}

	}

	static class CloseContextAndExit implements InitializingBean {

		private final ConfigurableApplicationContext context;

		private final Runnable shutdownHook;

		CloseContextAndExit(ConfigurableApplicationContext context, SpringApplicationShutdownHook shutdownHook) {
			this.context = context;
			this.shutdownHook = shutdownHook;
		}

		@Override
		public void afterPropertiesSet() throws Exception {
			this.context.close();
			// Simulate System.exit by running the hook on a separate thread and waiting
			// for it to complete
			Thread thread = new Thread(this.shutdownHook);
			thread.start();
			thread.join(15000);
			assertThat(thread.isAlive()).isFalse();
		}

	}

	static class FailingBean implements InitializingBean {

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

	}

}

Analyze Your Own Codebase

Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.

Try Supermodel Free