diff --git a/src/main/java/net/apnic/whowas/App.java b/src/main/java/net/apnic/whowas/App.java index 52225ff..8d939af 100644 --- a/src/main/java/net/apnic/whowas/App.java +++ b/src/main/java/net/apnic/whowas/App.java @@ -1,24 +1,11 @@ package net.apnic.whowas; -import java.util.Properties; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { - - public static void main(String[] args) - { - SpringApplication app = new SpringApplication(App.class); - Properties defaultProps = new Properties(); - - defaultProps.setProperty( - "spring.mvc.throw-exception-if-no-handler-found", "true"); - defaultProps.setProperty("spring.resources.add-mappings", "false"); - defaultProps.setProperty("spring.mvc.favicon.enabled", "false"); - defaultProps.setProperty("management.add-application-context-header", "false"); - app.setDefaultProperties(defaultProps); - app.run(args); + public static void main(String[] args) { + SpringApplication.run(App.class, args); } } diff --git a/src/main/java/net/apnic/whowas/loaders/RipeDbLoader.java b/src/main/java/net/apnic/whowas/loaders/RipeDbLoader.java index 62256c6..cf72649 100644 --- a/src/main/java/net/apnic/whowas/loaders/RipeDbLoader.java +++ b/src/main/java/net/apnic/whowas/loaders/RipeDbLoader.java @@ -20,17 +20,19 @@ import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; +@Component public class RipeDbLoader implements Loader { private static final Logger LOGGER = LoggerFactory.getLogger(RipeDbLoader.class); private long lastSerial; private final transient JdbcOperations operations; - public RipeDbLoader(JdbcOperations jdbcOperations, long serial) { - this.lastSerial = serial; + public RipeDbLoader(JdbcOperations jdbcOperations) { + this.lastSerial = -1; this.operations = jdbcOperations; } diff --git a/src/main/java/net/apnic/whowas/loaders/config/LoaderConfiguration.java b/src/main/java/net/apnic/whowas/loaders/config/LoaderConfiguration.java index 17f9795..bf28bd9 100644 --- a/src/main/java/net/apnic/whowas/loaders/config/LoaderConfiguration.java +++ b/src/main/java/net/apnic/whowas/loaders/config/LoaderConfiguration.java @@ -7,10 +7,7 @@ import java.time.temporal.ChronoUnit; import java.time.ZonedDateTime; import java.time.ZoneId; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; +import java.util.concurrent.*; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import javax.annotation.PostConstruct; @@ -33,9 +30,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.ApplicationContext; -import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.transaction.PlatformTransactionManager; @Configuration @EnableScheduling @@ -48,6 +47,8 @@ public class LoaderConfiguration @Autowired private ApplicationContext context; + + @Autowired private RipeDbLoader dbLoader; @Value("${snapshot.file:#{null}}") @@ -57,10 +58,14 @@ public class LoaderConfiguration History history; @Autowired - private JdbcOperations jdbcOperations; + SearchEngine searchEngine; + /* + This is autowired here to trigger Spring to initialise the transaction manager bean + before @PostConstruct is invoked. Without this, the initial data load will hang. + */ @Autowired - SearchEngine searchEngine; + PlatformTransactionManager platformTransactionManager; private void buildTree() { @@ -105,10 +110,19 @@ private void buildTree() } @PostConstruct - public void initialise() - { - dbLoader = new RipeDbLoader(jdbcOperations, -1L); - executorService.execute(this::buildTree); + public void initialise() throws ExecutionException, InterruptedException { + /* + Scoping the initial load of data to @PostConstruct + results in the loading completing before the servlet context is started, + preventing requests from being served from partially loaded data. + */ + Future initialDataLoaded = executorService.submit(this::buildTree); + initialDataLoaded.get(); + } + + @Bean + TaskScheduler taskScheduler() { + return new ThreadPoolTaskScheduler(); } @Scheduled(fixedRate = 15000L) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a67d6a8..c45e190 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,8 +30,17 @@ spring: username: "${database.username}" password: "${database.password}" + mvc: + throw-exception-if-no-handler-found: true + favicon: + enabled: false + resources: + add-mappings: false + + management: port: 8081 + add-application-context-header: false info: build: diff --git a/src/test/java/net/apnic/whowas/InitialisationTest.java b/src/test/java/net/apnic/whowas/InitialisationTest.java new file mode 100644 index 0000000..d373540 --- /dev/null +++ b/src/test/java/net/apnic/whowas/InitialisationTest.java @@ -0,0 +1,58 @@ +package net.apnic.whowas; + +import net.apnic.whowas.loaders.RipeDbLoader; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.ArrayList; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +public class InitialisationTest { + + @Test + public void initialDataIsLoadedBeforeServletIsStarted() { + ConcurrentLinkedQueue events = new ConcurrentLinkedQueue<>(); + + System.setProperty("server.port", "0"); + System.setProperty("management.port", "-1"); + + SpringApplication springApplication = new SpringApplication(App.class); + springApplication.addInitializers(configurableApplicationContext -> + configurableApplicationContext.addBeanFactoryPostProcessor(beanFactory -> + beanFactory.registerResolvableDependency( + RipeDbLoader.class, new RipeDbLoader(new JdbcTemplate()) { + @Override + public void loadWith(RevisionConsumer consumer) { + sleep(1000); + events.add("Data loaded"); + } + } + ) + ) + ); + springApplication.addListeners( + (ApplicationListener) applicationEvent -> + events.add("Servlet container started") + ); + + try(ConfigurableApplicationContext context = springApplication.run()) { + assertThat("Data was loaded before tomcat was initialised", + new ArrayList<>(events).get(0), is("Data loaded")); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +}