diff --git a/ci/validate-container b/ci/validate-container index d98df53e7a..c9ca9ce984 100755 --- a/ci/validate-container +++ b/ci/validate-container @@ -23,7 +23,7 @@ fi docker run --detach --rm --name $container_name camptocamp/mapfish_print:latest sleep 15 http_code=$(docker exec $container_name curl -sL -w "%{http_code}" -I localhost:8080/metrics/healthcheck -o /dev/null) -if [[ $http_code = 501 ]]; then +if [[ $http_code = 200 ]]; then echo "container healthy" cleanup exit 0 diff --git a/core/src/main/java/org/mapfish/print/metrics/ApplicationStatus.java b/core/src/main/java/org/mapfish/print/metrics/ApplicationStatus.java new file mode 100644 index 0000000000..96b214924f --- /dev/null +++ b/core/src/main/java/org/mapfish/print/metrics/ApplicationStatus.java @@ -0,0 +1,68 @@ +package org.mapfish.print.metrics; + +import com.codahale.metrics.health.HealthCheck; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import org.mapfish.print.servlet.job.JobQueue; +import org.mapfish.print.servlet.job.impl.ThreadPoolJobManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +class ApplicationStatus extends HealthCheck { + @Value("${healthStatus.expectedMaxTime.sinceLastPrint.InSeconds}") + private int secondsInFloatingWindow; + + @Value("${healthStatus.unhealthyThreshold.maxNbrPrintJobQueued}") + private int maxNbrPrintJobQueued; + + @Autowired private JobQueue jobQueue; + @Autowired private ThreadPoolJobManager jobManager; + + /** + * When a Result is returned it can be healthy or unhealthy. In both cases it is associated to a + * Http 200 status. When an exception is thrown it means the server is no longer working at all, + * it is associated to a Http 500 status. + */ + @Override + protected Result check() throws Exception { + long waitingJobsCount = jobQueue.getWaitingJobsCount(); + if (waitingJobsCount == 0) { + return Result.healthy("No print job is waiting in the queue."); + } + + String health = ". Number of print jobs waiting is " + waitingJobsCount; + + if (jobManager.getLastExecutedJobTimestamp() == null) { + return Result.unhealthy("No print job was ever processed by this server" + health); + } else if (hasThisServerPrintedRecently()) { + // WIP (See issue https://github.com/mapfish/mapfish-print/issues/3393) + if (waitingJobsCount > maxNbrPrintJobQueued) { + return Result.unhealthy( + "WIP: Number of print jobs queued is above threshold: " + + maxNbrPrintJobQueued + + health); + } else { + return Result.healthy("This server instance is printing" + health); + } + } else { + throw notificationForBrokenServer(); + } + } + + private RuntimeException notificationForBrokenServer() { + return new RuntimeException( + "None of the print job queued was processed by this server, in the last (seconds): " + + secondsInFloatingWindow); + } + + private boolean hasThisServerPrintedRecently() { + final Instant lastExecutedJobTime = jobManager.getLastExecutedJobTimestamp().toInstant(); + final Instant beginningOfTimeWindow = getBeginningOfTimeWindow(); + return lastExecutedJobTime.isAfter(beginningOfTimeWindow); + } + + private Instant getBeginningOfTimeWindow() { + return new Date().toInstant().minus(Duration.ofSeconds(secondsInFloatingWindow)); + } +} diff --git a/core/src/main/java/org/mapfish/print/metrics/HealthCheckingRegistry.java b/core/src/main/java/org/mapfish/print/metrics/HealthCheckingRegistry.java new file mode 100644 index 0000000000..4e1f4f3b59 --- /dev/null +++ b/core/src/main/java/org/mapfish/print/metrics/HealthCheckingRegistry.java @@ -0,0 +1,13 @@ +package org.mapfish.print.metrics; + +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; + +public class HealthCheckingRegistry extends com.codahale.metrics.health.HealthCheckRegistry { + @Autowired private ApplicationStatus applicationStatus; + + @PostConstruct + public void registerHealthCheck() { + register("application", applicationStatus); + } +} diff --git a/core/src/main/java/org/mapfish/print/servlet/job/JobManager.java b/core/src/main/java/org/mapfish/print/servlet/job/JobManager.java index 7ea15b8d52..79b0a8a8ed 100644 --- a/core/src/main/java/org/mapfish/print/servlet/job/JobManager.java +++ b/core/src/main/java/org/mapfish/print/servlet/job/JobManager.java @@ -1,8 +1,9 @@ package org.mapfish.print.servlet.job; +import java.util.Date; + /** Manages and Executes Print Jobs. */ public interface JobManager { - /** * Submit a new job for execution. * @@ -14,7 +15,7 @@ public interface JobManager { * Cancel a job. * * @param referenceId The referenceId of the job to cancel. - * @throws NoSuchReferenceException + * @throws NoSuchReferenceException When trying to cancel an unknown referenceId */ void cancel(String referenceId) throws NoSuchReferenceException; @@ -22,7 +23,14 @@ public interface JobManager { * Get the status for a job. * * @param referenceId The referenceId of the job to check. - * @throws NoSuchReferenceException + * @throws NoSuchReferenceException When requesting status of an unknown referenceId. */ PrintJobStatus getStatus(String referenceId) throws NoSuchReferenceException; + + /** + * Instant at which a job was executed by this manager. + * + * @return the timestamp as a Date. + */ + Date getLastExecutedJobTimestamp(); } diff --git a/core/src/main/java/org/mapfish/print/servlet/job/impl/ThreadPoolJobManager.java b/core/src/main/java/org/mapfish/print/servlet/job/impl/ThreadPoolJobManager.java index 8307e2dcb0..20dfe2b28d 100644 --- a/core/src/main/java/org/mapfish/print/servlet/job/impl/ThreadPoolJobManager.java +++ b/core/src/main/java/org/mapfish/print/servlet/job/impl/ThreadPoolJobManager.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -118,6 +119,7 @@ public class ThreadPoolJobManager implements JobManager { @Autowired private MetricRegistry metricRegistry; private boolean requestedToStop = false; + private Date lastExecutedJobTimestamp; public final void setMaxNumberOfRunningPrintJobs(final int maxNumberOfRunningPrintJobs) { this.maxNumberOfRunningPrintJobs = maxNumberOfRunningPrintJobs; @@ -290,7 +292,12 @@ public final void shutdown() { } } + public Date getLastExecutedJobTimestamp() { + return lastExecutedJobTimestamp; + } + private void executeJob(final PrintJob job) { + lastExecutedJobTimestamp = new Date(); LOGGER.debug( "executeJob {}, PoolSize {}, CorePoolSize {}, Active {}, Completed {}, Task {}, isShutdown" + " {}, isTerminated {}", diff --git a/core/src/main/resources/mapfish-spring-application-context.xml b/core/src/main/resources/mapfish-spring-application-context.xml index ff4ced5242..6583901b77 100644 --- a/core/src/main/resources/mapfish-spring-application-context.xml +++ b/core/src/main/resources/mapfish-spring-application-context.xml @@ -57,7 +57,8 @@ - + + diff --git a/core/src/main/resources/mapfish-spring.properties b/core/src/main/resources/mapfish-spring.properties index 7d59290bee..9bc9b61347 100644 --- a/core/src/main/resources/mapfish-spring.properties +++ b/core/src/main/resources/mapfish-spring.properties @@ -52,3 +52,9 @@ httpRequest.fetchRetry.maxNumber=3 # Number of milliseconds between 2 executions of the same request httpRequest.fetchRetry.intervalMillis=100 + +# Amount of time in the past where we check if a print job was executed by this server +healthStatus.expectedMaxTime.sinceLastPrint.InSeconds=300 + +# Maximum number of Print Jobs queued before raising it i +healthStatus.unhealthyThreshold.maxNbrPrintJobQueued=4 diff --git a/core/src/test/java/org/mapfish/print/metrics/ApplicationStatusTest.java b/core/src/test/java/org/mapfish/print/metrics/ApplicationStatusTest.java new file mode 100644 index 0000000000..e98c8a7737 --- /dev/null +++ b/core/src/test/java/org/mapfish/print/metrics/ApplicationStatusTest.java @@ -0,0 +1,86 @@ +package org.mapfish.print.metrics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import com.codahale.metrics.health.HealthCheck; +import java.util.Date; +import org.junit.Before; +import org.junit.Test; +import org.mapfish.print.AbstractMapfishSpringTest; +import org.mapfish.print.servlet.job.JobQueue; +import org.mapfish.print.servlet.job.impl.ThreadPoolJobManager; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; + +public class ApplicationStatusTest extends AbstractMapfishSpringTest { + + @Mock private JobQueue jobQueue; + + @Mock private ThreadPoolJobManager jobManager; + + @Autowired @InjectMocks private ApplicationStatus applicationStatus; + + @Before + public void setUp() throws Exception { + // Initialize mocks created above + MockitoAnnotations.openMocks(this); + } + + @Test + public void testCheck_Success_NoPrintJobs() throws Exception { + when(jobQueue.getWaitingJobsCount()).thenReturn(0L); + + HealthCheck.Result result = applicationStatus.check(); + + assertTrue(result.isHealthy()); + assertEquals("No print job is waiting in the queue.", result.getMessage()); + } + + @Test + public void testCheck_Failed_NoPrintJobs() throws Exception { + when(jobQueue.getWaitingJobsCount()).thenReturn(1L); + when(jobManager.getLastExecutedJobTimestamp()).thenReturn(new Date(0L)); + + RuntimeException rte = + assertThrowsExactly( + RuntimeException.class, + () -> { + // WHEN + applicationStatus.check(); + }); + + assertEquals( + "None of the print job queued was processed by this server, in the last (seconds): 300", + rte.getMessage()); + } + + @Test + public void testCheck_Success_PrintJobs() throws Exception { + when(jobQueue.getWaitingJobsCount()).thenReturn(5L, 4L); + when(jobManager.getLastExecutedJobTimestamp()).thenReturn(new Date()); + + applicationStatus.check(); + HealthCheck.Result result = applicationStatus.check(); + + assertTrue(result.isHealthy()); + assertTrue(result.getMessage().contains("This server instance is printing.")); + } + + @Test + public void testCheck_Fail_TooManyJobsAreQueued() throws Exception { + when(jobQueue.getWaitingJobsCount()).thenReturn(4L, 5L); + when(jobManager.getLastExecutedJobTimestamp()).thenReturn(new Date()); + + applicationStatus.check(); + HealthCheck.Result result = applicationStatus.check(); + + assertFalse(result.isHealthy()); + assertTrue(result.getMessage().contains("Number of print jobs queued is above threshold: ")); + } +} diff --git a/examples/src/test/java/org/mapfish/print/MetricsApiTest.java b/examples/src/test/java/org/mapfish/print/MetricsApiTest.java index 2cbc6f2e1a..778d6c2f77 100644 --- a/examples/src/test/java/org/mapfish/print/MetricsApiTest.java +++ b/examples/src/test/java/org/mapfish/print/MetricsApiTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -26,7 +27,7 @@ public class MetricsApiTest extends AbstractApiTest { @Test public void testMetrics() throws Exception { - ClientHttpRequest request = getMetricsRequest("metrics", HttpMethod.GET); + ClientHttpRequest request = getMetricsRequest("metrics"); try (ClientHttpResponse response = request.execute()) { assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); @@ -37,7 +38,7 @@ public void testMetrics() throws Exception { @Test public void testPing() throws Exception { - ClientHttpRequest request = getMetricsRequest("ping", HttpMethod.GET); + ClientHttpRequest request = getMetricsRequest("ping"); try (ClientHttpResponse response = request.execute()) { assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("pong", getBodyAsText(response).trim()); @@ -46,7 +47,7 @@ public void testPing() throws Exception { @Test public void testThreads() throws Exception { - ClientHttpRequest request = getMetricsRequest("threads", HttpMethod.GET); + ClientHttpRequest request = getMetricsRequest("threads"); try (ClientHttpResponse response = request.execute()) { assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); @@ -56,18 +57,19 @@ public void testThreads() throws Exception { @Test public void testHealthcheck() throws Exception { - ClientHttpRequest request = getMetricsRequest("healthcheck", HttpMethod.GET); + ClientHttpRequest request = getMetricsRequest("healthcheck"); try (ClientHttpResponse response = request.execute()) { - // TODO not implemented? - assertEquals(HttpStatus.NOT_IMPLEMENTED, response.getStatusCode()); - // assertEquals(HttpStatus.OK, response.getStatusCode()); - // assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); - // assertNotNull(new JSONObject(getBodyAsText(response))); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); + String bodyAsText = getBodyAsText(response); + assertNotNull(bodyAsText); + JSONObject healthcheck = new JSONObject(bodyAsText); + JSONObject application = healthcheck.getJSONObject("application"); + assertTrue(application.getBoolean("healthy")); } } - private ClientHttpRequest getMetricsRequest(String path, HttpMethod method) - throws IOException, URISyntaxException { - return getRequest("metrics/" + path, method); + private ClientHttpRequest getMetricsRequest(String path) throws IOException, URISyntaxException { + return getRequest("metrics/" + path, HttpMethod.GET); } } diff --git a/examples/src/test/java/org/mapfish/print/PrintApiTest.java b/examples/src/test/java/org/mapfish/print/PrintApiTest.java index 9664b64a3a..fbde22862c 100644 --- a/examples/src/test/java/org/mapfish/print/PrintApiTest.java +++ b/examples/src/test/java/org/mapfish/print/PrintApiTest.java @@ -44,7 +44,7 @@ public void testListApps() throws Exception { assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(getJsonMediaType(), response.getHeaders().getContentType()); final JSONArray appIdsJson = new JSONArray(getBodyAsText(response)); - assertTrue(appIdsJson.length() > 0); + assertFalse(appIdsJson.isEmpty()); } }