/* Copyright (c) 2001 - 2011 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.geoserver.wps.executor;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.opengis.wps10.ExecuteResponseType;
import org.apache.commons.io.IOUtils;
import org.geoserver.ows.XmlObjectEncodingResponse;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.wps.WPSException;
import org.geoserver.wps.executor.ExecutionStatus.ProcessState;
import org.geoserver.wps.ppio.ComplexPPIO;
import org.geoserver.wps.ppio.ProcessParameterIO;
import org.geoserver.wps.resource.WPSResourceManager;
import org.geoserver.wps.xml.WPSConfiguration;
import org.geotools.data.Parameter;
import org.geotools.process.ProcessException;
import org.geotools.process.ProcessFactory;
import org.geotools.process.Processors;
import org.geotools.util.logging.Logging;
import org.opengis.feature.type.Name;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
/**
* Manages the process runs for both synchronous and asynchronous processes
*
* @author Andrea Aime - GeoSolutions
*
*/
public class WPSExecutionManager implements ApplicationContextAware,
ApplicationListener<ApplicationEvent> {
private static final Logger LOGGER = Logging.getLogger(WPSExecutionManager.class);
private ExecutorService storedResponseWriters = Executors.newCachedThreadPool();
ApplicationContext applicationContext;
private WPSResourceManager resourceManager;
private List<ProcessManager> processManagers;
private Map<String, AsynchronousProcessContext> contexts = new ConcurrentHashMap<String, AsynchronousProcessContext>();
private int connectionTimeout;
public WPSExecutionManager(WPSResourceManager resourceManager) {
this.resourceManager = resourceManager;
}
/**
* This call should only be used by process chaining to avoid deadlocking due to execution
* threads starvation
*
* @param request
* @return
*/
Map<String, Object> submitChained(ExecuteRequest request) {
Name processName = request.getProcessName();
ProcessManager processManager = getProcessManager(processName);
String executionId = resourceManager.getExecutionId(true);
Map<String, Object> inputs = request.getProcessInputs(this);
return processManager.submitChained(executionId, processName, inputs);
}
/**
* Process submission, not blocking. Returns an id that can be used to get the process status
* and result later.
*
* @param ExecuteType The request to be executed
* @param inputs The process inputs
* @return The execution id
* @throws ProcessException
*/
public String submit(ExecuteRequest request, boolean synchronous) throws ProcessException {
Name processName = request.getProcessName();
ProcessManager processManager = getProcessManager(processName);
LazyInputMap inputs = request.getProcessInputs(this);
String executionId = resourceManager.getExecutionId(synchronous);
final AsynchronousProcessContext context = new AsynchronousProcessContext(request,
executionId, inputs, processManager, applicationContext);
contexts.put(executionId, context);
processManager.submit(executionId, processName, inputs, request.isAsynchronous());
if (request.isAsynchronous()) {
// ah, we need to store the output at the end, schedule a thread that will
// do as soon as the process is done executing
storedResponseWriters.submit(new Runnable() {
@Override
public void run() {
context.writeResponseFile();
}
});
}
return executionId;
}
/**
* Returns the status response for an asynch call if the id is known, null otherwise (it means
* the process is either unknown or its execution already completed, in the latter case calling
* getStoredResponse(executionId) will provide the stored response, assuming not too much time
* passed between the end of the execution and the
*/
public ExecuteResponseType getStatus(String executionId) {
AsynchronousProcessContext context = contexts.get(executionId);
if (context == null) {
return null;
}
return context.getStatusResponse();
}
/**
* Returns the stored response file for the specified execution (which has already completed its
* lifecycle)
*
* @param executionId
* @return
*/
public File getStoredResponse(String executionId) {
return resourceManager.getStoredResponseFile(executionId);
}
public File getStoredOutput(String executionId, String outputId) {
return resourceManager.getOutputFile(executionId, outputId);
}
/**
* Cancels a process
*
* @param executionId
*/
public void cancel(String executionId) {
AsynchronousProcessContext context = contexts.get(executionId);
if (context != null) {
context.processManager.cancel(executionId);
}
}
/**
* Returns the execute response for synch requests. This call is blocking, the caller will be
* blocked until the process completes both input parsing and execution. The code will throw an
* exception is the process is to be executed in stored mode.
*
* @param executionId
* @return
*/
public Map<String, Object> getOutput(String executionId, long timeout) throws ProcessException {
for (ProcessManager pm : getProcessManagers()) {
Map<String, Object> output = pm.getOutput(executionId, timeout);
if (output != null) {
contexts.remove(executionId);
return output;
}
}
throw new ProcessException("Failed to find output for execution " + executionId);
}
List<ProcessManager> getProcessManagers() {
if (processManagers == null) {
synchronized (this) {
if (processManagers == null) {
processManagers = GeoServerExtensions.extensions(ProcessManager.class);
}
}
}
return processManagers;
}
ProcessManager getProcessManager(Name processName) {
for (ProcessManager pm : getProcessManagers()) {
if (pm.canHandle(processName)) {
return pm;
}
}
throw new WPSException("Could not find a ProcessManager able to run this process: "
+ processName);
}
public int getConnectionTimeout() {
return connectionTimeout;
}
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.applicationContext = context;
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent) {
if (storedResponseWriters == null) {
storedResponseWriters = Executors.newCachedThreadPool();
} else if (event instanceof ContextClosedEvent) {
storedResponseWriters.shutdownNow();
}
}
}
public class AsynchronousProcessContext {
String executionId;
LazyInputMap inputs;
ProcessManager processManager;
ExecuteRequest request;
volatile Exception exception;
Date started;
private float inputWeight;
private float outputWeight;
private float processWeight;
public AsynchronousProcessContext(ExecuteRequest request, String executionId,
LazyInputMap inputs, ProcessManager processManager,
ApplicationContext applicationContext) {
this.request = request;
this.executionId = executionId;
this.inputs = inputs;
this.processManager = processManager;
this.started = new Date();
// there are three fases running a process
// 1 - retrieve and parse inputs
// 2 - process
// 3 - encode outputs
// Here we have a simple heuristics to guess how long each one is
this.inputWeight = inputs.longParse() ? 0.33f : 0f;
this.outputWeight = hasComplexOutputs() ? 0.33f : 0f;
this.processWeight = 1 - inputWeight - outputWeight;
}
boolean hasComplexOutputs() {
ProcessFactory pf = Processors.createProcessFactory(request.getProcessName());
Map<String, Parameter<?>> resultInfo = pf.getResultInfo(request.getProcessName(), inputs);
for (Parameter<?> param : resultInfo.values()) {
List<ProcessParameterIO> ppios = ProcessParameterIO.findAll(param, applicationContext);
for (ProcessParameterIO ppio : ppios) {
if(ppio instanceof ComplexPPIO) {
return true;
}
}
}
return false;
}
ExecutionStatus getOverallStatus() {
ExecutionStatus inner = processManager.getStatus(executionId);
// the process already completed?
if (inner == null || inner.phase == ProcessState.COMPLETED) {
if (exception != null) {
// failed
return new ExecutionStatus(request.getProcessName(), executionId, ProcessState.COMPLETED, 1f);
} else {
// Still running, it's writing the output. Right now we have no way to track the
// output progress, so return 66% complete
return new ExecutionStatus(request.getProcessName(), executionId, ProcessState.RUNNING,
0.66f);
}
} else {
// still running
float progress = inputs.getRetrievedInputPercentage() * inputWeight;
progress += inner.getProgress() * processWeight;
return new ExecutionStatus(request.getProcessName(), executionId, ProcessState.RUNNING, progress);
}
}
ExecuteResponseType getStatusResponse() {
ExecutionStatus overallStatus;
if (request.isStatusEnabled()) {
// user requested to get status updates
overallStatus = getOverallStatus();
} else {
// only stored, we won't give updates until the process is completed (the
// spec demands this, "If status is "false" then the Status element shall not be
// updated until the process either completes successfully or fails)
overallStatus = new ExecutionStatus(request.getProcessName(), executionId, ProcessState.QUEUED,
0f);
}
ExecuteResponseBuilder responseBuilder = new ExecuteResponseBuilder(request.getRequest(),
applicationContext, started);
responseBuilder.setExecutionId(executionId);
responseBuilder.setStatus(overallStatus);
responseBuilder.setException(exception);
return responseBuilder.build();
}
public void writeResponseFile() {
try {
resourceManager.setCurrentExecutionId(executionId);
ExecuteResponseBuilder responseBuilder = new ExecuteResponseBuilder(
request.getRequest(), applicationContext, started);
responseBuilder.setExecutionId(executionId);
try {
Map<String, Object> outputs = processManager.getOutput(executionId, -1);
responseBuilder.setOutputs(outputs);
} catch (Exception exception) {
responseBuilder.setException(exception);
}
// write to a temp file (as that might take time) and only when done switch to the
// actual output file
File output = resourceManager.getStoredResponseFile(executionId);
try {
writeOutResponse(responseBuilder, output);
} catch (Exception e) {
// maybe it was an exception during output encoding, try to write out
// the error if possible
LOGGER.log(Level.SEVERE, "Request failed during output encoding", e);
responseBuilder.setException(e);
writeOutResponse(responseBuilder, output);
}
} catch (Exception e) {
// ouch, this is bad, we can just log the output...
LOGGER.log(Level.SEVERE,
"Failed to write out the stored WPS response for executionId "
+ executionId, e);
} finally {
contexts.remove(executionId);
}
}
void writeOutResponse(ExecuteResponseBuilder responseBuilder, File output)
throws IOException {
FileOutputStream fos = null;
File tmpOutput = new File(output.getParent(), "tmp" + output.getName());
try {
ExecuteResponseType response = responseBuilder.build();
XmlObjectEncodingResponse encoder = new XmlObjectEncodingResponse(
ExecuteResponseType.class, "ExecuteResponse", WPSConfiguration.class);
fos = new FileOutputStream(tmpOutput);
encoder.write(response, fos, null);
fos.flush();
fos.close();
if (!tmpOutput.renameTo(output)) {
LOGGER.log(Level.SEVERE, "Failed to rename " + tmpOutput + " to " + output);
}
} finally {
IOUtils.closeQuietly(fos);
if (tmpOutput != null) {
tmpOutput.delete();
}
}
}
}
}