diff --git a/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerException.java b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerException.java new file mode 100644 index 00000000..cbcd6ba4 --- /dev/null +++ b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerException.java @@ -0,0 +1,24 @@ +package io.sease.rre.server.services; + +/** + * Exception thrown during evaluation handling. + * + * @author Matt Pearce (matt@flax.co.uk) + */ +public class EvaluationHandlerException extends Exception { + + public EvaluationHandlerException() { + } + + public EvaluationHandlerException(String message) { + super(message); + } + + public EvaluationHandlerException(String message, Throwable cause) { + super(message, cause); + } + + public EvaluationHandlerException(Throwable cause) { + super(cause); + } +} diff --git a/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerService.java b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerService.java index c5980dcd..fb6faedc 100644 --- a/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerService.java +++ b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerService.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import io.sease.rre.core.domain.Evaluation; +import io.sease.rre.server.domain.EvaluationMetadata; import org.springframework.stereotype.Service; /** @@ -10,16 +11,36 @@ * and use them to build an Evaluation object that can be used to populate * the dashboard. * + * The {@link #processEvaluationRequest(JsonNode)} method should ideally + * return as quickly as possible, to avoid blocking the sender of the incoming + * request. The evaluation data can then be retrieved using {@link #getEvaluation()} + * where the evaluation contains the most recently processed data. + * * @author Matt Pearce (matt@flax.co.uk) */ @Service public interface EvaluationHandlerService { /** - * Update the currently held evaluation data. + * Update the currently held evaluation data. This may be done + * asynchronously - the method should return as quickly as possible. * * @param requestData incoming data giving details of evaluation. - * @throws Exception if the data cannot be processed. + * @throws EvaluationHandlerException if the data cannot be processed. + */ + void processEvaluationRequest(final JsonNode requestData) throws EvaluationHandlerException; + + /** + * Get the current evaluation data. + * + * @return the Evaluation. + */ + Evaluation getEvaluation(); + + /** + * Get the current evaluation metadata. + * + * @return the evaluation metadata. */ - Evaluation processEvaluationRequest(final JsonNode requestData) throws Exception; + EvaluationMetadata getEvaluationMetadata(); } diff --git a/rre-server/src/main/java/io/sease/rre/server/services/HttpEvaluationHandlerService.java b/rre-server/src/main/java/io/sease/rre/server/services/HttpEvaluationHandlerService.java index 9247d8fe..75f9e019 100644 --- a/rre-server/src/main/java/io/sease/rre/server/services/HttpEvaluationHandlerService.java +++ b/rre-server/src/main/java/io/sease/rre/server/services/HttpEvaluationHandlerService.java @@ -3,12 +3,16 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.sease.rre.core.domain.*; +import io.sease.rre.server.domain.EvaluationMetadata; import io.sease.rre.server.domain.StaticMetric; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import static java.util.stream.StreamSupport.stream; @@ -25,9 +29,27 @@ public class HttpEvaluationHandlerService implements EvaluationHandlerService { private final ObjectMapper mapper = new ObjectMapper(); + private Evaluation evaluation = new Evaluation(); + private EvaluationMetadata metadata = new EvaluationMetadata(Collections.emptyList(), Collections.emptyList()); + + @Override + public void processEvaluationRequest(final JsonNode requestData) throws EvaluationHandlerException { + evaluation = make(requestData); + } + + @Override + public Evaluation getEvaluation() { + return evaluation; + } + @Override - public Evaluation processEvaluationRequest(final JsonNode requestData) { - return make(requestData); + public EvaluationMetadata getEvaluationMetadata() { + return metadata; + } + + void setEvaluation(Evaluation eval) { + this.evaluation = eval; + this.metadata = extractEvaluationMetadata(eval); } ObjectMapper getMapper() { @@ -99,4 +121,24 @@ private void metrics(final JsonNode data, final DomainMember parent) { parent.getMetrics().put(metric.getName(), metric); }); } + + /** + * Extract the evaluation metadata from an evaluation. + * + * @param evaluation the evaluation data. + * @return the evaluation metadata. + */ + public static EvaluationMetadata extractEvaluationMetadata(final Evaluation evaluation) { + final List metrics = new ArrayList<>( + evaluation.getChildren() + .iterator().next() + .getMetrics().keySet()); + + final List versions = new ArrayList<>( + evaluation.getChildren() + .iterator().next() + .getMetrics().values().iterator().next().getVersions().keySet()); + + return new EvaluationMetadata(versions, metrics); + } } diff --git a/rre-server/src/main/java/io/sease/rre/server/services/URLEvaluationHandlerService.java b/rre-server/src/main/java/io/sease/rre/server/services/URLEvaluationHandlerService.java index e16e26fc..d9fe38ea 100644 --- a/rre-server/src/main/java/io/sease/rre/server/services/URLEvaluationHandlerService.java +++ b/rre-server/src/main/java/io/sease/rre/server/services/URLEvaluationHandlerService.java @@ -10,6 +10,8 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; /** * Implementation of the evaluation handler that extracts a URL from the @@ -24,28 +26,62 @@ public class URLEvaluationHandlerService extends HttpEvaluationHandlerService im private static final Logger LOGGER = LoggerFactory.getLogger(URLEvaluationHandlerService.class); - @Override - public Evaluation processEvaluationRequest(JsonNode requestData) { - Evaluation eval = new Evaluation(); + private URLEvaluationUpdater updater = null; + @Override + public void processEvaluationRequest(JsonNode requestData) throws EvaluationHandlerException { try { + if (updater != null && updater.isAlive()) { + throw new EvaluationHandlerException("Update is already running - request rejected!"); + } + final String urlParam = requestData.get("url").asText(); - final JsonNode evaluationNode = readNodeFromUrl(new URL(urlParam)); + LOGGER.debug("Extracted URL {} from incoming request", urlParam); - eval = make(evaluationNode); + // Build the evaluation in a separate thread - avoid causing timeouts in the report plugin + updater = createUpdaterThread(new URL(urlParam)); + updater.start(); } catch (IOException e) { LOGGER.error("Caught IOException processing request: {}", e.getMessage()); + throw new EvaluationHandlerException(e); } + } - return eval; + private URLEvaluationUpdater createUpdaterThread(URL evaluationUrl) { + URLEvaluationUpdater thread = new URLEvaluationUpdater(evaluationUrl); + // Run the thread in the background + thread.setDaemon(true); + return thread; } - private JsonNode readNodeFromUrl(URL evaluationUrl) throws IOException { - try { - return getMapper().readTree(evaluationUrl); - } catch (IOException e) { - LOGGER.error("Caught IOException reading JSON from {}: {}", evaluationUrl, e.getMessage()); - throw e; + + class URLEvaluationUpdater extends Thread { + + private final URL evaluationUrl; + + URLEvaluationUpdater(URL evaluationUrl) { + this.evaluationUrl = evaluationUrl; + } + + @Override + public void run() { + try { + LOGGER.info("Building evaluation from URL {}", evaluationUrl); + final JsonNode evaluationNode = readNodeFromUrl(evaluationUrl); + setEvaluation(make(evaluationNode)); + LOGGER.debug("Evaluation build complete"); + } catch (IOException e) { + LOGGER.error("Caught IOException building evaluation: {}", e.getMessage()); + } + } + + private JsonNode readNodeFromUrl(URL evaluationUrl) throws IOException { + try { + return getMapper().readTree(evaluationUrl); + } catch (IOException e) { + LOGGER.error("Caught IOException reading JSON from {}: {}", evaluationUrl, e.getMessage()); + throw e; + } } } }