Commit d6478b2a by Johannes Edmeier

Prevent threads from surviving the application context shutdown.

As it turns out (rather obvious) that the ThreadPools for registration and status updating needs to be destroyed on application shutdown so that the worker threads gets cancelled. To achieve this we simply register the ThreadPools as spring beans (since they are implementing DisposableBean). fixes #253
parent c8be9561
......@@ -18,9 +18,9 @@ package de.codecentric.boot.admin.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.ApplicationContext;
......@@ -34,7 +34,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.StringUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
......@@ -53,6 +54,7 @@ import de.codecentric.boot.admin.journal.web.JournalController;
import de.codecentric.boot.admin.registry.ApplicationIdGenerator;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
import de.codecentric.boot.admin.registry.StatusUpdateApplicationListener;
import de.codecentric.boot.admin.registry.StatusUpdater;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
......@@ -195,9 +197,26 @@ public class AdminServerWebConfiguration extends WebMvcConfigurerAdapter
return statusUpdater;
}
@Bean
@Qualifier("updateTaskScheduler")
public TaskScheduler updateTaskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(1);
taskScheduler.setRemoveOnCancelPolicy(true);
taskScheduler.setThreadNamePrefix("updateTask");
return taskScheduler;
}
@Bean
public StatusUpdateApplicationListener statusUpdateApplicationListener() {
StatusUpdateApplicationListener listener = new StatusUpdateApplicationListener(
statusUpdater(), updateTaskScheduler());
listener.setUpdatePeriod(adminServerProperties().getMonitor().getPeriod());
return listener;
}
@EventListener
public void onClientApplicationRegistered(ClientApplicationRegisteredEvent event) {
statusUpdater().updateStatus(event.getApplication());
publisher.publishEvent(new RoutesOutdatedEvent());
}
......@@ -207,22 +226,6 @@ public class AdminServerWebConfiguration extends WebMvcConfigurerAdapter
}
@Bean
public ScheduledTaskRegistrar updateTaskRegistrar() {
return new ScheduledTaskRegistrar();
}
@EventListener
public void onApplicationReadyEvent(ApplicationReadyEvent event) {
updateTaskRegistrar().addFixedRateTask(new Runnable() {
@Override
public void run() {
statusUpdater().updateStatusForAllApplications();
}
}, adminServerProperties().getMonitor().getPeriod());
updateTaskRegistrar().afterPropertiesSet();
}
@Bean
@ConditionalOnMissingBean
public ApplicationEventJournal applicationEventJournal() {
return new ApplicationEventJournal(journaledEventStore());
......
package de.codecentric.boot.admin.registry;
import java.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.TaskScheduler;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
public class StatusUpdateApplicationListener {
private static final Logger LOGGER = LoggerFactory.getLogger(StatusUpdateApplicationListener.class);
private final TaskScheduler taskScheduler;
private final StatusUpdater statusUpdater;
private long updatePeriod = 10_000L;
private ScheduledFuture<?> scheduledTask;
public StatusUpdateApplicationListener(StatusUpdater statusUpdater,
TaskScheduler taskScheduler) {
this.statusUpdater = statusUpdater;
this.taskScheduler = taskScheduler;
}
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
startStatusUpdate();
}
@EventListener
public void onContextClosed(ContextClosedEvent event) {
stopStatusUpdate();
}
@EventListener
public void onClientApplicationRegistered(ClientApplicationRegisteredEvent event) {
statusUpdater.updateStatus(event.getApplication());
}
public void startStatusUpdate() {
if (scheduledTask != null && !scheduledTask.isDone()) {
return;
}
scheduledTask = taskScheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
statusUpdater.updateStatusForAllApplications();
}
}, updatePeriod);
LOGGER.debug("Scheduled status-updater task for every {}ms", updatePeriod);
}
public void stopStatusUpdate() {
if (scheduledTask != null && !scheduledTask.isDone()) {
scheduledTask.cancel(true);
LOGGER.debug("Canceled status-updater task");
}
}
public void setUpdatePeriod(long updatePeriod) {
this.updatePeriod = updatePeriod;
}
}
package de.codecentric.boot.admin.registry;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.concurrent.ScheduledFuture;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedWebApplicationContext;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.TaskScheduler;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.model.Application;
public class StatusUpdateApplicationListenerTest {
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void test_start_stop() throws Exception {
StatusUpdater statusUpdater = mock(StatusUpdater.class);
TaskScheduler scheduler = mock(TaskScheduler.class);
StatusUpdateApplicationListener listener = new StatusUpdateApplicationListener(
statusUpdater, scheduler);
ScheduledFuture task = mock(ScheduledFuture.class);
when(scheduler.scheduleAtFixedRate(isA(Runnable.class), eq(10_000L))).thenReturn(task);
listener.onApplicationReady(
new ApplicationReadyEvent(mock(SpringApplication.class), null, null));
verify(scheduler).scheduleAtFixedRate(isA(Runnable.class), eq(10_000L));
listener.onContextClosed(new ContextClosedEvent(mock(EmbeddedWebApplicationContext.class)));
verify(task).cancel(true);
}
@Test
public void test_newApplication() throws Exception {
StatusUpdater statusUpdater = mock(StatusUpdater.class);
TaskScheduler scheduler = mock(TaskScheduler.class);
StatusUpdateApplicationListener listener = new StatusUpdateApplicationListener(
statusUpdater, scheduler);
Application application = Application.create("test").withHealthUrl("http://example.com")
.build();
listener.onClientApplicationRegistered(new ClientApplicationRegisteredEvent(application));
verify(statusUpdater).updateStatus(eq(application));
}
}
......@@ -16,6 +16,7 @@
package de.codecentric.boot.admin.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
......@@ -24,6 +25,8 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import de.codecentric.boot.admin.services.ApplicationRegistrator;
import de.codecentric.boot.admin.services.RegistrationApplicationListener;
......@@ -57,6 +60,16 @@ public class SpringBootAdminClientAutoConfiguration {
return new ApplicationRegistrator(builder.build(), admin, client);
}
@Bean
@Qualifier("registrationTaskScheduler")
public TaskScheduler registrationTaskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(1);
taskScheduler.setRemoveOnCancelPolicy(true);
taskScheduler.setThreadNamePrefix("registrationTask");
return taskScheduler;
}
/**
* ApplicationListener triggering registration after being ready/shutdown
*/
......@@ -64,7 +77,7 @@ public class SpringBootAdminClientAutoConfiguration {
@ConditionalOnMissingBean
public RegistrationApplicationListener registrationListener() {
RegistrationApplicationListener listener = new RegistrationApplicationListener(
registrator());
registrator(), registrationTaskScheduler());
listener.setAutoRegister(admin.isAutoRegistration());
listener.setAutoDeregister(admin.isAutoDeregistration());
listener.setRegisterPeriod(admin.getPeriod());
......
......@@ -15,8 +15,6 @@
*/
package de.codecentric.boot.admin.services;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
......@@ -27,7 +25,6 @@ import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
/**
* Listener responsible for starting and stopping the registration task when the application is
......@@ -44,22 +41,12 @@ public class RegistrationApplicationListener {
private long registerPeriod = 10_000L;
private volatile ScheduledFuture<?> scheduledTask;
public RegistrationApplicationListener(ApplicationRegistrator registrator,
TaskScheduler taskScheduler) {
this.registrator = registrator;
this.taskScheduler = taskScheduler;
}
public RegistrationApplicationListener(ApplicationRegistrator registrator,
ScheduledExecutorService scheduler) {
this(registrator, new ConcurrentTaskScheduler(scheduler));
}
public RegistrationApplicationListener(ApplicationRegistrator registrator) {
this(registrator, Executors.newSingleThreadScheduledExecutor());
}
@EventListener
@Order(Ordered.LOWEST_PRECEDENCE)
public void onApplicationReady(ApplicationReadyEvent event) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment