add job service to be able to cancel plot requests
This commit is contained in:
@@ -8,6 +8,10 @@ public class InternalServerError extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 548651821080252932L;
|
||||
|
||||
public InternalServerError(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InternalServerError(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -28,6 +30,8 @@ import org.lucares.pdbui.domain.FilterDefaults;
|
||||
import org.lucares.pdbui.domain.PlotRequest;
|
||||
import org.lucares.pdbui.domain.PlotResponse;
|
||||
import org.lucares.pdbui.domain.PlotResponseStats;
|
||||
import org.lucares.pdbui.job.Job;
|
||||
import org.lucares.pdbui.job.JobService;
|
||||
import org.lucares.performance.db.PerformanceDb;
|
||||
import org.lucares.recommind.logs.InternalPlottingException;
|
||||
import org.lucares.recommind.logs.NoDataPointsException;
|
||||
@@ -65,8 +69,6 @@ public class PdbController implements HardcodedValues, PropertyKeys {
|
||||
private final Plotter plotter;
|
||||
private final PerformanceDb db;
|
||||
|
||||
private final ReentrantLock plotterLock = new ReentrantLock();
|
||||
|
||||
@Value("${" + DEFAULTS_QUERY_EXAMPLES + ":}")
|
||||
private String queryExamples;
|
||||
|
||||
@@ -78,10 +80,14 @@ public class PdbController implements HardcodedValues, PropertyKeys {
|
||||
|
||||
private final CsvUploadHandler csvUploadHandler;
|
||||
|
||||
public PdbController(final PerformanceDb db, final Plotter plotter, final CsvUploadHandler csvUploadHandler) {
|
||||
private final JobService jobService;
|
||||
|
||||
public PdbController(final PerformanceDb db, final Plotter plotter, final CsvUploadHandler csvUploadHandler,
|
||||
final JobService jobService) {
|
||||
this.db = db;
|
||||
this.plotter = plotter;
|
||||
this.csvUploadHandler = csvUploadHandler;
|
||||
this.jobService = jobService;
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/plots", //
|
||||
@@ -100,32 +106,65 @@ public class PdbController implements HardcodedValues, PropertyKeys {
|
||||
|
||||
// TODO the UI should cancel requests that are in flight before sending a plot
|
||||
// request
|
||||
if (plotterLock.tryLock(5, TimeUnit.SECONDS)) {
|
||||
try {
|
||||
final PlotResult result = plotter.plot(plotSettings);
|
||||
final Future<ResponseEntity<PlotResponse>> future = jobService
|
||||
.runJob(new Job<ResponseEntity<PlotResponse>>(request.getSubmitterId()) {
|
||||
|
||||
final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName();
|
||||
LOGGER.trace("image url: {}", imageUrl);
|
||||
@Override
|
||||
public ResponseEntity<PlotResponse> executeJob() {
|
||||
try {
|
||||
final PlotResult result = plotter.plot(plotSettings);
|
||||
|
||||
final String thumbnailUrl = result.getThumbnailPath() != null
|
||||
? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName()
|
||||
: "img/no-thumbnail.png";
|
||||
final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName();
|
||||
LOGGER.trace("image url: {}", imageUrl);
|
||||
|
||||
final PlotResponseStats stats = PlotResponseStats.fromDataSeries(result.getDataSeries());
|
||||
final PlotResponse plotResponse = new PlotResponse(stats, imageUrl, thumbnailUrl);
|
||||
final String thumbnailUrl = result.getThumbnailPath() != null
|
||||
? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName()
|
||||
: "img/no-thumbnail.png";
|
||||
|
||||
return ResponseEntity.ok().body(plotResponse);
|
||||
} catch (final NoDataPointsException e) {
|
||||
throw new NotFoundException("No data was found. Try another query, or change the date range.", e);
|
||||
} finally {
|
||||
plotterLock.unlock();
|
||||
final PlotResponseStats stats = PlotResponseStats.fromDataSeries(result.getDataSeries());
|
||||
final PlotResponse plotResponse = new PlotResponse(stats, imageUrl, thumbnailUrl);
|
||||
|
||||
return ResponseEntity.ok().body(plotResponse);
|
||||
} catch (final NoDataPointsException e) {
|
||||
throw new NotFoundException(
|
||||
"No data was found. Try another query, or change the date range.", e);
|
||||
} catch (final InternalPlottingException e) {
|
||||
throw new InternalServerError("Internal Server Error", e);
|
||||
} catch (final RejectedExecutionException e) {
|
||||
throw new ServiceUnavailableException("Too many parallel requests!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final long deadline = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5);
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
try {
|
||||
return future.get(1, TimeUnit.SECONDS);
|
||||
} catch (final TimeoutException e) {
|
||||
LOGGER.info("job for submitter {} still running", request.getSubmitterId());
|
||||
} catch (final CancellationException e) {
|
||||
throw new PlotAbortedWebException();
|
||||
}
|
||||
}
|
||||
LOGGER.info("job for submitter {} timed out, will cancel the job", request.getSubmitterId());
|
||||
future.cancel(true);
|
||||
throw new InternalServerError("Internal Server Error");
|
||||
|
||||
} else {
|
||||
throw new ServiceUnavailableException("Too many parallel requests!");
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
throw new InternalServerError("Internal Server Error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/plots/{submitterId}", //
|
||||
method = RequestMethod.DELETE)
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void cancelJob(@PathVariable("submitterId") final String submitterId)
|
||||
throws InternalPlottingException, InterruptedException {
|
||||
jobService.cancel(submitterId);
|
||||
}
|
||||
|
||||
/*
|
||||
* @RequestMapping(path = "/plots", // method = RequestMethod.GET, // produces =
|
||||
* MediaType.APPLICATION_OCTET_STREAM_VALUE // ) StreamingResponseBody
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.lucares.pdbui;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(value = HttpStatus.CONFLICT, reason = "Request Aborted")
|
||||
public class PlotAbortedWebException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -601662865253785050L;
|
||||
|
||||
}
|
||||
@@ -39,6 +39,8 @@ public class PlotRequest {
|
||||
|
||||
private boolean renderBarChartTickLabels;
|
||||
|
||||
private String submitterId;
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
@@ -179,4 +181,13 @@ public class PlotRequest {
|
||||
public void setRenderBarChartTickLabels(final boolean renderBarChartTickLabels) {
|
||||
this.renderBarChartTickLabels = renderBarChartTickLabels;
|
||||
}
|
||||
|
||||
public String getSubmitterId() {
|
||||
return submitterId;
|
||||
}
|
||||
|
||||
public void setSubmitterId(final String submitterId) {
|
||||
this.submitterId = submitterId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
16
pdb-ui/src/main/java/org/lucares/pdbui/job/Job.java
Normal file
16
pdb-ui/src/main/java/org/lucares/pdbui/job/Job.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.lucares.pdbui.job;
|
||||
|
||||
public abstract class Job<T> {
|
||||
|
||||
private final String submitterId;
|
||||
|
||||
public Job(final String submitterId) {
|
||||
this.submitterId = submitterId;
|
||||
}
|
||||
|
||||
public String getSubmitterId() {
|
||||
return submitterId;
|
||||
}
|
||||
|
||||
public abstract T executeJob();
|
||||
}
|
||||
14
pdb-ui/src/main/java/org/lucares/pdbui/job/JobContext.java
Normal file
14
pdb-ui/src/main/java/org/lucares/pdbui/job/JobContext.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package org.lucares.pdbui.job;
|
||||
|
||||
public class JobContext {
|
||||
|
||||
private JobState jobState = JobState.ACTIVE;
|
||||
|
||||
public void cancelJob() {
|
||||
jobState = JobState.CANCELLED;
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return jobState == JobState.CANCELLED;
|
||||
}
|
||||
}
|
||||
87
pdb-ui/src/main/java/org/lucares/pdbui/job/JobService.java
Normal file
87
pdb-ui/src/main/java/org/lucares/pdbui/job/JobService.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package org.lucares.pdbui.job;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JobService implements AutoCloseable {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JobService.class);
|
||||
|
||||
private static final int MAX_JOBS = 1;
|
||||
|
||||
private final ExecutorService threadPool = new ThreadPoolExecutor(0, MAX_JOBS, 1L, TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<>(1), new CustomizableThreadFactory("jobs"));
|
||||
|
||||
private final Map<String, Future<?>> jobs = new ConcurrentHashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Future<T> runJob(final Job<T> job) {
|
||||
|
||||
return (Future<T>) jobs.compute(job.getSubmitterId(), (submitterId, oldFuture) -> {
|
||||
|
||||
if (oldFuture != null) {
|
||||
LOGGER.info("cancelling old job");
|
||||
oldFuture.cancel(true);
|
||||
try {
|
||||
oldFuture.get(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
LOGGER.info("old job finished with exception", e);
|
||||
} catch (final CancellationException e) {
|
||||
LOGGER.info("cancelled job for submitter {}", submitterId);
|
||||
} catch (final TimeoutException e) {
|
||||
LOGGER.info("old job did not finish within 5 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
return threadPool.submit(() -> {
|
||||
try {
|
||||
LOGGER.info("starting job for submitter {}", job.getSubmitterId());
|
||||
return job.executeJob();
|
||||
} finally {
|
||||
jobs.remove(job.getSubmitterId());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
threadPool.shutdownNow();
|
||||
threadPool.awaitTermination(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void cancel(final String submitterId) {
|
||||
|
||||
jobs.computeIfPresent(submitterId, (_submitterId, jobFuture) -> {
|
||||
|
||||
LOGGER.info("cancelling old job");
|
||||
jobFuture.cancel(true);
|
||||
try {
|
||||
jobFuture.get(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
LOGGER.info("old job finished with exception", e);
|
||||
} catch (final CancellationException e) {
|
||||
LOGGER.info("cancelled job for submitter {}", submitterId);
|
||||
} catch (final TimeoutException e) {
|
||||
LOGGER.info("old job did not finish within 5 seconds");
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
5
pdb-ui/src/main/java/org/lucares/pdbui/job/JobState.java
Normal file
5
pdb-ui/src/main/java/org/lucares/pdbui/job/JobState.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package org.lucares.pdbui.job;
|
||||
|
||||
public enum JobState {
|
||||
ACTIVE, CANCELLED
|
||||
}
|
||||
Reference in New Issue
Block a user