All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.routing.impl.InputStreamGraphSource Maven / Gradle / Ivy

package org.opentripplanner.routing.impl;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.google.common.io.ByteStreams;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.services.GraphSource;
import org.opentripplanner.routing.services.StreetVertexIndexFactory;
import org.opentripplanner.standalone.Router;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;

/**
 * The primary implementation of the GraphSource interface. The graph is loaded from a serialized
 * graph from a given source.
 * 
 */
public class InputStreamGraphSource implements GraphSource {

    public static final String GRAPH_FILENAME = "Graph.obj";

    private static final Logger LOG = LoggerFactory.getLogger(InputStreamGraphSource.class);

    /**
     * Delay before starting to load a graph after the last modification time. In case of writing,
     * we expect graph last modification time to be updated at at least that frequency. If not, you
     * can either increase this value, or use an atomic move when copying the file.
     * */
    private static final long LOAD_DELAY_SEC = 10;

    private Router router;

    private String routerId;

    private long graphLastModified = 0L;

    private Object preEvictMutex = new Boolean(false);

    /**
     * The current used input stream implementation for getting graph data source.
     */
    private Streams streams;

    // TODO Why do we need a factory? There is a single one implementation.
    private StreetVertexIndexFactory streetVertexIndexFactory = new DefaultStreetVertexIndexFactory();

    /**
     * @return A GraphSource loading graph from the file system under a base path.
     */
    public static InputStreamGraphSource newFileGraphSource(String routerId, File path) {
        return new InputStreamGraphSource(routerId, new FileStreams(path));
    }

    /**
     * @return A GraphSource loading graph from an embedded classpath resources (a graph bundled
     *         inside a pre-packaged WAR for example).
     */
    public static InputStreamGraphSource newClasspathGraphSource(String routerId, File path) {
        return new InputStreamGraphSource(routerId, new ClasspathStreams(path));
    }

    private InputStreamGraphSource(String routerId, Streams streams) {
        this.routerId = routerId;
        this.streams = streams;
    }

    @Override
    public Router getRouter() {
        /*
         * We synchronize on pre-evict mutex in case we are in the middle of reloading in pre-evict
         * mode. In that case we must make the client wait until the new graph is loaded, because
         * the old one is gone to the GC. Performance hit should be low as getGraph() is not called
         * often.
         */
        synchronized (preEvictMutex) {
            return router;
        }
    }

    @Override
    public boolean reload(boolean force, boolean preEvict) {
        /* We synchronize on 'this' to prevent multiple reloads from being called at the same time */
        synchronized (this) {
            long lastModified = streams.getLastModified();
            boolean doReload = force ? true : checkAutoReload(lastModified);
            if (!doReload)
                return true;
            if (preEvict) {
                synchronized (preEvictMutex) {
                    if (router != null) {
                        LOG.info("Reloading '{}': pre-evicting router", routerId);
                        router.shutdown();
                    }
                    /*
                     * Forcing router to null here should remove any references to the graph once
                     * all current requests are done. So the next reload is supposed to have more
                     * memory.
                     */
                    router = null;
                    router = loadGraph();
                }
            } else {
                Router newRouter = loadGraph();
                if (newRouter != null) {
                    // Load OK
                    if (router != null) {
                        LOG.info("Reloading '{}': post-evicting router", routerId);
                        router.shutdown();
                    }
                    router = newRouter; // Assignment in java is atomic
                } else {
                    // Load failed
                    if (force || router == null) {
                        LOG.warn("Unable to load data for router '{}'.", routerId);
                        if (router != null) {
                            router.shutdown();
                        }
                        router = null;
                    } else {
                        // No shutdown, since we keep current one.
                        LOG.warn("Unable to load data for router '{}', keeping old data.", routerId);
                    }
                }
            }
            if (router == null) {
                graphLastModified = 0L;
            } else {
                /*
                 * Note: we flag even if loading failed, because we want to wait for fresh new data
                 * before loading again.
                 */
                graphLastModified = lastModified;
            }
            // If a router is null, it will be evicted.
            return (router != null);
        }
    }

    /**
     * Check if a graph has been modified since the last time it has been loaded.
     * 
     * @param lastModified Time of last modification of current loaded data.
     * @return True if the input data has been modified and need to be reloaded.
     */
    private boolean checkAutoReload(long lastModified) {
        // We check only for graph file modification, not config
        long validEndTime = System.currentTimeMillis() - LOAD_DELAY_SEC * 1000;
        LOG.debug(
                "checkAutoReload router '{}' validEndTime={} lastModified={} graphLastModified={}",
                routerId, validEndTime, lastModified, graphLastModified);
        if (lastModified != graphLastModified && lastModified <= validEndTime) {
            // Only reload graph modified more than 1 mn ago.
            LOG.info("Router ID '{}' graph input modification detected, force reload.", routerId);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void evict() {
        synchronized (this) {
            if (router != null) {
                router.shutdown();
                router = null;
            }
        }
    }

    /**
     * Do the actual operation of graph loading. Load configuration if present, and startup the
     * router with the help of the router lifecycle manager.
     */
    private Router loadGraph() {
        final Graph newGraph;
        try (InputStream is = streams.getGraphInputStream()) {
            LOG.info("Loading graph...");
            try {
                newGraph = Graph.load(is);
            } catch (Exception ex) {
                LOG.error("Exception while loading graph '{}'.", routerId, ex);
                return null;
            }

            newGraph.routerId = (routerId);
        } catch (IOException e) {
            LOG.warn("Graph file not found or not openable for routerId '{}': {}", routerId, e);
            return null;
        }

        // Decorate the graph TODO how are we "decorating" it? This appears to refer to loading its configuration.
        // Even if a config file is not present on disk one could be bundled inside.
        try (InputStream is = streams.getConfigInputStream()) {
            JsonNode config = MissingNode.getInstance();
            // TODO reuse the exact same JSON loader from OTPConfigurator
            ObjectMapper mapper = new ObjectMapper();
            mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
            mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
            if (is != null) {
                config = mapper.readTree(is);
            } else if (newGraph.routerConfig != null) {
                config = mapper.readTree(newGraph.routerConfig);
            }
            Router newRouter = new Router(routerId, newGraph);
            newRouter.startup(config);
            return newRouter;
        } catch (IOException e) {
            LOG.error("Can't read config file.");
            LOG.error(e.getMessage());
            return null;
        }
    }

    /**
     * InputStreamGraphSource delegates to some actual implementation the fact of getting the input
     * stream and checking the last modification timestamp for a given routerId.
     * FIXME this seems like a lot of boilerplate just to switch between FileInputStream and getResourceAsStream
     * a couple of conditional blocks and a boolean field "onClasspath" might do the trick.
     */
    private interface Streams {
        public abstract InputStream getGraphInputStream() throws IOException;

        public abstract InputStream getConfigInputStream() throws IOException;

        public abstract long getLastModified();
    }

    private static class FileStreams implements Streams {

        private File path;

        private FileStreams(File path) {
            this.path = path;
        }

        @Override
        public InputStream getGraphInputStream() throws IOException {
            File graphFile = new File(path, GRAPH_FILENAME);
            LOG.debug("Loading graph from file '{}'", graphFile.getPath());
            return new FileInputStream(graphFile);
        }

        @Override
        public InputStream getConfigInputStream() throws IOException {
            File configFile = new File(path, Router.ROUTER_CONFIG_FILENAME);
            if (configFile.canRead()) {
                LOG.debug("Loading config from file '{}'", configFile.getPath());
                return new FileInputStream(configFile);
            } else {
                return null;
            }
        }

        @Override
        public long getLastModified() {
            // Note: this returns 0L if the file does not exists
            return new File(path, GRAPH_FILENAME).lastModified();
        }
    }

    private static class ClasspathStreams implements Streams {

        private File path;

        private ClasspathStreams(File path) {
            this.path = path;
        }

        @Override
        public InputStream getGraphInputStream() {
            File graphFile = new File(path, GRAPH_FILENAME);
            LOG.debug("Loading graph from classpath at '{}'", graphFile.getPath());
            return Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(graphFile.getPath());
        }

        @Override
        public InputStream getConfigInputStream() {
            File configFile = new File(path, Router.ROUTER_CONFIG_FILENAME);
            LOG.debug("Trying to load config on classpath at '{}'", configFile.getPath());
            return Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(configFile.getPath());
        }

        /**
         * For a packaged classpath resources we assume the data won't change, so returning always
         * 0L basically disable auto-reload in that case.
         */
        @Override
        public long getLastModified() {
            return 0L;
        }
    }

    /**
     * A GraphSource factory creating InputStreamGraphSource from file.
     */
    public static class FileFactory implements GraphSource.Factory {

        private static final Logger LOG = LoggerFactory.getLogger(FileFactory.class);

        public File basePath;

        public FileFactory(File basePath) {
            this.basePath = basePath;
        }

        @Override
        public GraphSource createGraphSource(String routerId) {
            return InputStreamGraphSource.newFileGraphSource(routerId, getBasePath(routerId));
        }

        @Override
        public boolean save(String routerId, InputStream is) {

            File sourceFile = new File(getBasePath(routerId), InputStreamGraphSource.GRAPH_FILENAME);

            try {

                // Create directory if necessary
                File directory = new File(sourceFile.getParentFile().getPath());
                if (!directory.exists()) {
                    directory.mkdir();
                }

                // Store the stream to disk, to be sure no data will be lost make a temporary backup
                // file of the original file.

                // Make backup file
                File destFile = null;
                if (sourceFile.exists()) {
                    destFile = new File(sourceFile.getPath() + ".bak");
                    if (destFile.exists()) {
                        destFile.delete();
                    }
                    sourceFile.renameTo(destFile);
                }

                // Store the stream
                try (FileOutputStream os = new FileOutputStream(sourceFile)) {
                    ByteStreams.copy(is, os);
                }

                // And delete the backup file
                sourceFile = new File(sourceFile.getPath() + ".bak");
                if (sourceFile.exists()) {
                    sourceFile.delete();
                }

            } catch (Exception ex) {
                LOG.error("Exception while storing graph to {}.", sourceFile.getPath(), ex);
                return false;
            }

            return true;
        }

        private File getBasePath(String routerId) {
            return new File(basePath, routerId);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy