/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wps.executor; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.platform.ExtensionPriority; import org.geoserver.threadlocals.ThreadLocalsTransfer; import org.geoserver.wps.WPSException; import org.geoserver.wps.executor.ExecutionStatus.ProcessState; import org.geoserver.wps.process.GeoServerProcessors; import org.geoserver.wps.resource.WPSResourceManager; import org.geotools.process.Process; import org.geotools.process.ProcessException; import org.geotools.process.ProcessFactory; import org.geotools.util.logging.Logging; import org.opengis.feature.type.Name; import org.opengis.util.InternationalString; import org.opengis.util.ProgressListener; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; public class DefaultProcessManager implements ProcessManager, ExtensionPriority, ApplicationListener<ApplicationEvent> { ConcurrentHashMap<String, ExecutionStatusEx> executions = new ConcurrentHashMap<String, DefaultProcessManager.ExecutionStatusEx>(); ThreadPoolExecutor synchService; ThreadPoolExecutor asynchService; WPSResourceManager resourceManager; public DefaultProcessManager(WPSResourceManager resourceManager) { this.resourceManager = resourceManager; } public void setMaxAsynchronousProcesses(int maxAsynchronousProcesses) { if(asynchService == null) { // create a fixed size pool. If we allow a delta between core and max // the pool will create new threads only if the queue is full, but the linked queue never is asynchService = new ThreadPoolExecutor(maxAsynchronousProcesses, maxAsynchronousProcesses, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } else { asynchService.setCorePoolSize(maxAsynchronousProcesses); asynchService.setMaximumPoolSize(maxAsynchronousProcesses); } } public void setMaxSynchronousProcesses(int maxSynchronousProcesses) { if(synchService == null) { // create a fixed size pool. If we allow a delta between core and max // the pool will create new threads only if the queue is full, but the linked queue never is synchService = new ThreadPoolExecutor(maxSynchronousProcesses, maxSynchronousProcesses, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } else { synchService.setCorePoolSize(maxSynchronousProcesses); synchService.setMaximumPoolSize(maxSynchronousProcesses); } } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextRefreshedEvent) { if (event instanceof ContextClosedEvent) { synchService.shutdownNow(); asynchService.shutdownNow(); } } } /** * We can handle everything, other process managers will have to use a higher priority than us */ @Override public boolean canHandle(Name processName) { return true; } @Override public Map<String, Object> submitChained(String executionId, Name processName, Map<String, Object> inputs) throws ProcessException { // straight execution, no thread pooling, we're already running in the parent process thread ProcessListener listener = new ProcessListener(new ExecutionStatus(processName, executionId, ProcessState.RUNNING, 0, null)); ProcessFactory pf = GeoServerProcessors.createProcessFactory(processName); if (pf == null) { throw new WPSException("No such process: " + processName); } // execute the process in the same thread as the caller Process p = pf.create(processName); Map<String, Object> result = p.execute(inputs, listener); if (listener.exception != null) { throw new ProcessException("Process failed: " + listener.exception.getMessage(), listener.exception); } return result; } @Override public void submit(String executionId, Name processName, Map<String, Object> inputs, boolean background) throws ProcessException { ExecutionStatusEx status = new ExecutionStatusEx(processName, executionId); ProcessListener listener = new ProcessListener(status); status.listener = listener; ProcessCallable callable = new ProcessCallable(inputs, status); Future<Map<String, Object>> future; if(background) { future = asynchService.submit(callable); } else { future = synchService.submit(callable); } status.future = future; executions.put(executionId, status); } protected ExecutionStatusEx createExecutionStatus(Name processName, String executionId) { return new ExecutionStatusEx(processName, executionId); } @Override public ExecutionStatus getStatus(String executionId) { ExecutionStatusEx status = executions.get(executionId); if (status != null) { return status.getStatus(); } else { return null; } } @Override public Map<String, Object> getOutput(String executionId, long timeout) throws ProcessException { ExecutionStatusEx status = executions.get(executionId); if (status == null) { return null; } try { if (timeout <= 0) { return status.future.get(); } else { return status.future.get(timeout, TimeUnit.MILLISECONDS); } } catch (Exception e) { if(e instanceof ExecutionException && e.getCause() instanceof Exception) { e = (Exception) e.getCause(); } if (e instanceof ProcessException) { throw (ProcessException) e; } else { throw new ProcessException("Process execution " + executionId + " failed", e); } } finally { // we're done executions.remove(executionId); } } @Override public void cancel(String executionId) { ExecutionStatusEx status = executions.get(executionId); if (status != null) { status.setPhase(ProcessState.CANCELLED); status.future.cancel(true); status.listener.setCanceled(true); } } @Override public List<ExecutionStatus> getRunningProcesses() { List<ExecutionStatus> result = new ArrayList<ExecutionStatus>(); for (ExecutionStatusEx status : executions.values()) { result.add(status.getStatus()); } return result; } @Override public int getPriority() { return ExtensionPriority.LOWEST; } class ProcessCallable implements Callable<Map<String, Object>> { Map<String, Object> inputs; ProcessListener listener; ExecutionStatusEx status; ThreadLocalsTransfer threadLocalTransfer; public ProcessCallable(Map<String, Object> inputs, ExecutionStatusEx status) { this.inputs = inputs; this.status = status; this.threadLocalTransfer = new ThreadLocalsTransfer(); } @Override public Map<String, Object> call() throws Exception { try { // transfer the thread locals to this execution context threadLocalTransfer.apply(); resourceManager.setCurrentExecutionId(status.getExecutionId()); status.setPhase(ProcessState.RUNNING); ProcessListener listener = status.listener; Name processName = status.getProcessName(); ProcessFactory pf = GeoServerProcessors.createProcessFactory(processName); if (pf == null) { throw new WPSException("No such process: " + processName); } // execute the process Map<String, Object> result = null; try { Process p = pf.create(processName); result = p.execute(inputs, listener); if (listener.exception != null) { status.setPhase(ProcessState.FAILED); throw new WPSException("Process failed: " + listener.exception.getMessage(), listener.exception); } return result; } finally { // update status unless cancelled if (status.getPhase() == ProcessState.RUNNING) { status.setPhase(ProcessState.COMPLETED); } } } finally { // clean up the thread locals threadLocalTransfer.cleanup(); } } } /** * A pimped up execution status * * @author Andrea Aime - GeoSolutions */ static class ExecutionStatusEx extends ExecutionStatus { Future<Map<String, Object>> future; ProcessListener listener; public ExecutionStatusEx(Name processName, String executionId) { super(processName, executionId, ProcessState.QUEUED, 0, null); } public ExecutionStatus getStatus() { return new ExecutionStatus(processName, executionId, phase, progress, task); } @Override public void setPhase(ProcessState phase) { if (phase == ProcessState.CANCELLED && getPhase() != ProcessState.CANCELLED) { future.cancel(true); listener.setCanceled(true); } super.setPhase(phase); } @Override public Map<String, Object> getOutput(long timeout) throws Exception { if (timeout <= 0) { return future.get(); } else { return future.get(timeout, TimeUnit.MILLISECONDS); } } } /** * Listens to the process progress and allows to cancel it * * @author Andrea Aime - GeoSolutions */ static class ProcessListener implements ProgressListener { static final Logger LOGGER = Logging.getLogger(ProcessListener.class); ExecutionStatus status; InternationalString task; String description; Throwable exception; public ProcessListener(ExecutionStatus status) { this.status = status; } @Override public InternationalString getTask() { return task; } @Override public void setTask(InternationalString task) { this.task = task; if(task != null) { status.setTask(task.toString()); } } @Override public String getDescription() { return this.description; } @Override public void setDescription(String description) { this.description = description; } @Override public void started() { status.setPhase(ProcessState.RUNNING); } @Override public void progress(float percent) { status.setProgress(percent); } @Override public float getProgress() { return status.getProgress(); } @Override public void complete() { status.setPhase(ProcessState.COMPLETED); } @Override public void dispose() { // nothing to do } @Override public boolean isCanceled() { return status.getPhase() == ProcessState.CANCELLED; } @Override public void setCanceled(boolean cancel) { if (cancel == true) { status.setPhase(ProcessState.CANCELLED); } } @Override public void warningOccurred(String source, String location, String warning) { LOGGER.log(Level.WARNING, "Got a warning during process execution " + status.getExecutionId() + ": " + warning); } @Override public void exceptionOccurred(Throwable exception) { this.exception = exception; } } }