/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.routing.impl; import java.io.StreamCorruptedException; import java.util.HashMap; import java.util.Map; import org.opentripplanner.routing.services.PatchService; import org.opentripplanner.routing.services.PathService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ApplicationContext; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.StaticApplicationContext; /** * Factory keep a cache of previously created path services. If running low in memory, it will * remove oldest accessed graph. If the underlying graph file has been modified since last load, it * will be automatically reloaded. */ public abstract class GenericMultiplePathServiceFactory { private static final int MAX_RETRIES = 3; private static final int RETRY_INTERVAL = 3000; private static final Logger LOG = LoggerFactory .getLogger(GenericMultiplePathServiceFactory.class); private static class SubServicesBundle { public AbstractApplicationContext context; public long timestampLoaded; public PathService pathService; public PatchService patchService; public boolean reloadInProgress; } private String[] subApplicationContextList; private boolean asyncReload = false; private Map<String, SubServicesBundle> subServices = new HashMap<String, SubServicesBundle>(); /** * @param subApplicationContext Spring application configuration to use for instantiating each * service. */ public void setSubApplicationContextList(String[] subApplicationContextList) { this.subApplicationContextList = subApplicationContextList; } /** * @param asyncReload Set async reload mode: If true, reload the new graph in the background * while serving an old version to incoming requests. If false, block all incoming * requests until new graph is reloaded. Please be aware that activating asyncReload will * greatly increase memory requirements. */ public void setAsyncReload(boolean asyncReload) { this.asyncReload = asyncReload; } /** * @param routerID * @param timestamp The timestamp to check against. * @return True if the data source has been modified since "timestamp". */ protected abstract boolean checkReload(String routerID, long timestamp); /** * Register needed data source in the given application context. * * @param context The application context to register the data source. * @param registry The bean registry. */ protected abstract void registerDataSource(String routerID, ApplicationContext context, BeanDefinitionRegistry registry); protected PathService doGetPathService(String routerID) { return doGetSubServiceBundle(routerID).pathService; } protected PatchService doGetPatchService(String routerID) { return doGetSubServiceBundle(routerID).patchService; } private SubServicesBundle doGetSubServiceBundle(String routerID) { /* Ensure we have a PathServiceBundle, even an empty one, to synchronize on. */ SubServicesBundle ssb = null; synchronized (subServices) { ssb = subServices.get(routerID); if (ssb == null) { ssb = new SubServicesBundle(); ssb.pathService = null; ssb.timestampLoaded = System.currentTimeMillis(); ssb.reloadInProgress = false; subServices.put(routerID, ssb); } } /* * Synchronize access to the bundle only, to prevent blocking requests to other graphs. * Check for first-time loading, no background reload is possible since no older version is * available. */ synchronized (ssb) { if (ssb.pathService == null) { SubServicesBundle newSsb = loadSubServices(routerID); ssb.pathService = newSsb.pathService; ssb.patchService = newSsb.patchService; ssb.context = newSsb.context; ssb.timestampLoaded = System.currentTimeMillis(); return ssb; } } /* Here background reload becomes possible. */ boolean reload = false; synchronized (ssb) { if (checkReload(routerID, ssb.timestampLoaded) && !ssb.reloadInProgress) { if (!asyncReload) { LOG.info("Reloading modified graph '" + routerID + "'"); // Sync reload: remove old version before loading new one, in synchronized // block. ssb.pathService = null; ssb.patchService = null; ssb.context.close(); ssb.context = null; SubServicesBundle newSsb = loadSubServices(routerID); ssb.pathService = newSsb.pathService; ssb.patchService = newSsb.patchService; ssb.context = newSsb.context; ssb.timestampLoaded = System.currentTimeMillis(); } else { reload = true; ssb.reloadInProgress = true; } } } if (reload) { // Async reload: load new version but keep old one while not ready for other // requests. SubServicesBundle newSsb = loadSubServices(routerID); synchronized (ssb) { LOG.info("Async reloading modified graph '" + routerID + "'"); ssb.pathService = newSsb.pathService; ssb.patchService = newSsb.patchService; ssb.context.close(); ssb.context = newSsb.context; ssb.reloadInProgress = false; ssb.timestampLoaded = System.currentTimeMillis(); } } return ssb; } /** * Construct and load a new SubServicesBundle. Use default Spring application configuration to * build a new set of services. * * @param routerID * @return A new PathService instance from a new ApplicationContext. */ private SubServicesBundle loadSubServices(String routerID) { /* * Create a parent context containing the bundle. */ AbstractApplicationContext parentContext = new StaticApplicationContext(); BeanDefinitionRegistry registry = (BeanDefinitionRegistry) parentContext; registerDataSource(routerID, parentContext, registry); parentContext.refresh(); int retries = 0; SubServicesBundle retval = new SubServicesBundle(); while (true) { try { /* * Create a new context to create a new path service, with all dependents services, * using the default application context definition. The creation of a new context * allow us to create new instances of service beans. */ retval.context = new ClassPathXmlApplicationContext(subApplicationContextList, parentContext); AutowireCapableBeanFactory factory = retval.context.getAutowireCapableBeanFactory(); retval.pathService = (PathService) factory .getBean("pathService", PathService.class); try { retval.patchService = (PatchService) factory.getBean("patchService", PatchService.class); } catch (NoSuchBeanDefinitionException e) { LOG.warn("No bean 'patchService' defined in application-context.xml, skipping it."); } break; } catch (BeanCreationException e) { /* * Copying a new graph should use an atomic copy, but for convenience if it is not, * we retry for a few times in case of truncated data before bailing out. * * The original StreamCorruptedException is buried within dozen of layers of other * exceptions, so we have to dig a bit (is this considered a hack?). */ boolean streamCorrupted = false; Throwable t = e.getCause(); while (t != null) { if (t instanceof StreamCorruptedException) { streamCorrupted = true; break; } t = t.getCause(); } if (!streamCorrupted || retries++ > MAX_RETRIES) throw e; LOG.warn("Can't load " + routerID + " (" + e + "): retrying..."); try { Thread.sleep(RETRY_INTERVAL); } catch (InterruptedException e1) { } } } return retval; } }