/* 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.resource; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.ows.DispatcherCallback; import org.geoserver.ows.Request; import org.geoserver.ows.Response; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.Operation; import org.geoserver.platform.Service; import org.geoserver.platform.ServiceException; import org.geoserver.platform.resource.Resource; import org.geoserver.wps.WPSException; import org.geotools.util.logging.Logging; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextStoppedEvent; /** * A WPS process has to deal with various temporary resources during the execution, be streamed and * stored inputs, Sextante temporary files, temporary feature types and so on. * * This class manages the lifecycle of these resources, register them here to have their lifecycle * properly managed * * The design is still very rough, I'm making this up as I go. The class will require modifications * to handle asynch process computations as well as resources with a timeout * * @author Andrea Aime - GeoSolutions * * TODO: add methods to support process locking and all the deferred cleanup required for asynch processes */ public class WPSResourceManager implements DispatcherCallback, ApplicationListener<ApplicationEvent> { private static final Logger LOGGER = Logging.getLogger(WPSResourceManager.class); ConcurrentHashMap<String, ExecutionResources> resourceCache = new ConcurrentHashMap<String, ExecutionResources>(); ThreadLocal<String> executionId = new InheritableThreadLocal<String>(); static final class ExecutionResources { /** * Temporary resources used to parse inputs or during the process execution */ List<WPSResource> temporary; /** * Resources representing process outputs, should be kept around for some time for asynch * processes */ List<WPSResource> outputs; /** Whether the execution is synchronous or asynch */ boolean synchronouos; /** If true there is something accessing the output files and preventing their deletion */ boolean outputLocked; /** Marks the process completion, we start counting down for output deletion */ long completionTime; public ExecutionResources(boolean synchronouos) { this.synchronouos = synchronouos; this.temporary = new ArrayList<WPSResource>(); this.outputs = new ArrayList<WPSResource>(); } } /** * Create a new unique id for the process. All resources linked to the process should use this * token to register themselves against the manager * * @return */ public String getExecutionId(Boolean synch) { String id = executionId.get(); if (id == null) { id = UUID.randomUUID().toString(); executionId.set(id); resourceCache.put(id, new ExecutionResources(synch != null ? synch : true)); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Associating process with execution id: " + id); } } return id; } /** * ProcessManagers should call this method every time they are running the process in a thread * other than the request thread, and that is not a child of it either (typical case is running * in a thread pool) * * @param executionId */ public void setCurrentExecutionId(String executionId) { ExecutionResources resources = resourceCache.get(executionId); if (resources == null) { throw new IllegalStateException("Execution id " + executionId + " is not known"); } this.executionId.set(executionId); } public void addResource(WPSResource resource) { String processId = getExecutionId(null); ExecutionResources resources = resourceCache.get(processId); if (resources == null) { throw new IllegalStateException("The executionId was not set for the current thread!"); } else { resources.temporary.add(resource); } } /** * Returns a file that will be used to store a process output as a "reference" * * @param executionId * @param fileName * @return */ public File getOutputFile(String executionId, String fileName) { File outputDirectory = new File(getWpsOutputStorage(), executionId); if(!outputDirectory.exists()) { mkdir(outputDirectory); } return new File(outputDirectory, fileName); } /** * Returns a file that will be used to store some temporary file for processing sake, and will * mark it for deletion when the process ends * * @param executionId * @param fileName * @return * @throws IOException */ public File getTemporaryFile(String extension) throws IOException { String processId = getExecutionId(null); File outputDirectory = new File(getWpsOutputStorage(), processId); if (!outputDirectory.exists()) { mkdir(outputDirectory); } File file = File.createTempFile("tmp", extension, outputDirectory); addResource(new WPSFileResource(file)); return file; } private void mkdir(File file) { if(!file.mkdir()) { throw new WPSException("Failed to create the specified directory " + file); } } /** * Gets the stored response file for the specified execution id * @param executionId * @return */ public File getStoredResponseFile(String executionId) { File file = new File(getWpsOutputStorage(), executionId + ".xml"); return file; } public File getWpsOutputStorage() { File wpsStore = null; try { GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class); Resource wps = loader.get("temp/wps"); wpsStore = wps.dir(); // find or create } catch(Exception e) { throw new ServiceException("Could not create the temporary storage directory for WPS"); } if(wpsStore == null || !wpsStore.exists()) { throw new ServiceException("Could not create the temporary storage directory for WPS"); } return wpsStore; } // ----------------------------------------------------------------- // DispatcherCallback methods // ----------------------------------------------------------------- public void finished(Request request) { // if we did not generate any process id, no resources have been added if (executionId.get() == null) { return; } // grab the id and unbind the thread local String id = executionId.get(); executionId.remove(); // cleanup automatically if the process is synchronous if (resourceCache.get(id).synchronouos) { cleanProcess(id); resourceCache.remove(id); } } public void finished(String executionId) { // cleanup the thread local, in case it has any id in it this.executionId.remove(); // cleanup the temporary resources cleanProcess(executionId); // mark the process as complete resourceCache.get(executionId).completionTime = System.currentTimeMillis(); } /** * Cleans up all the resources associated to a certain id. It is called automatically * when the request ends for synchronous processes, for asynch ones it will be triggered * by the process completion * * @param id */ void cleanProcess(String id) { // delete all resources associated with the process ExecutionResources executionResources = resourceCache.get(id); for (WPSResource resource : executionResources.temporary) { try { resource.delete(); } catch (Throwable t) { LOGGER.log(Level.WARNING, "Failed to clean up the WPS resource " + resource.getName(), t); } } } public Request init(Request request) { return null; } public Operation operationDispatched(Request request, Operation operation) { return null; } public Object operationExecuted(Request request, Operation operation, Object result) { return null; } public Response responseDispatched(Request request, Operation operation, Object result, Response response) { return null; } public Service serviceDispatched(Request request, Service service) throws ServiceException { return null; } // ----------------------------------------------------------------- // ApplicationListener methods // ----------------------------------------------------------------- public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextClosedEvent || event instanceof ContextStoppedEvent) { // we are shutting down, remove all temp resources! for (String id : resourceCache.keySet()) { cleanProcess(id); } resourceCache.clear(); } } }