add job service to be able to cancel plot requests

This commit is contained in:
2023-02-18 17:36:54 +01:00
parent 8c410fac4a
commit ed448af78c
18 changed files with 296 additions and 38 deletions

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}
}

View 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();
}

View 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;
}
}

View 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;
});
}
}

View File

@@ -0,0 +1,5 @@
package org.lucares.pdbui.job;
public enum JobState {
ACTIVE, CANCELLED
}